mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-24 06:28:55 -05:00
Compare commits
6 Commits
main
...
add_appris
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abf47fac6e | ||
|
|
97e98aba47 | ||
|
|
e1dc68eb85 | ||
|
|
f6230d295a | ||
|
|
d79c88bc85 | ||
|
|
5aba4bb2c6 |
@@ -23,10 +23,6 @@ ARG PACKAGES_PAT
|
||||
WORKDIR /app
|
||||
EXPOSE 11011
|
||||
|
||||
# Copy solution and project files first for better layer caching
|
||||
# COPY backend/*.sln ./backend/
|
||||
# COPY backend/*/*.csproj ./backend/*/
|
||||
|
||||
# Copy source code
|
||||
COPY backend/ ./backend/
|
||||
|
||||
@@ -48,13 +44,21 @@ RUN --mount=type=cache,target=/root/.nuget/packages,sharing=locked \
|
||||
# Runtime stage
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
|
||||
|
||||
# Install required packages for user management and timezone support
|
||||
# Install required packages for user management, timezone support, and Python for Apprise CLI
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
tzdata \
|
||||
gosu \
|
||||
python3 \
|
||||
python3-venv \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create virtual environment and install Apprise CLI
|
||||
ENV VIRTUAL_ENV=/opt/apprise-venv
|
||||
RUN python3 -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN pip install --no-cache-dir apprise==1.9.6
|
||||
|
||||
ENV PUID=1000 \
|
||||
PGID=1000 \
|
||||
UMASK=022 \
|
||||
|
||||
@@ -12,6 +12,8 @@ public static class NotificationsDI
|
||||
services
|
||||
.AddScoped<INotifiarrProxy, NotifiarrProxy>()
|
||||
.AddScoped<IAppriseProxy, AppriseProxy>()
|
||||
.AddScoped<IAppriseCliProxy, AppriseCliProxy>()
|
||||
.AddSingleton<IAppriseCliDetector, AppriseCliDetector>()
|
||||
.AddScoped<INtfyProxy, NtfyProxy>()
|
||||
.AddScoped<IPushoverProxy, PushoverProxy>()
|
||||
.AddScoped<INotificationConfigurationService, NotificationConfigurationService>()
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Api.Features.Notifications.Contracts.Requests;
|
||||
|
||||
public record CreateAppriseProviderRequest : CreateNotificationProviderRequestBase
|
||||
{
|
||||
public AppriseMode Mode { get; init; } = AppriseMode.Api;
|
||||
|
||||
// API mode fields
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
|
||||
public string Tags { get; init; } = string.Empty;
|
||||
|
||||
// CLI mode fields
|
||||
public string? ServiceUrls { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Api.Features.Notifications.Contracts.Requests;
|
||||
|
||||
public record TestAppriseProviderRequest
|
||||
{
|
||||
public AppriseMode Mode { get; init; } = AppriseMode.Api;
|
||||
|
||||
// API mode fields
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
|
||||
public string Tags { get; init; } = string.Empty;
|
||||
|
||||
// CLI mode fields
|
||||
public string? ServiceUrls { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
|
||||
namespace Cleanuparr.Api.Features.Notifications.Contracts.Requests;
|
||||
|
||||
public record UpdateAppriseProviderRequest : UpdateNotificationProviderRequestBase
|
||||
{
|
||||
public AppriseMode Mode { get; init; } = AppriseMode.Api;
|
||||
|
||||
// API mode fields
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
|
||||
public string Tags { get; init; } = string.Empty;
|
||||
|
||||
// CLI mode fields
|
||||
public string? ServiceUrls { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
using System.Net;
|
||||
using Cleanuparr.Api.Features.Notifications.Contracts.Requests;
|
||||
using Cleanuparr.Api.Features.Notifications.Contracts.Responses;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Api.Features.Notifications.Controllers;
|
||||
|
||||
@@ -21,17 +21,20 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly INotificationConfigurationService _notificationConfigurationService;
|
||||
private readonly NotificationService _notificationService;
|
||||
private readonly IAppriseCliDetector _appriseCliDetector;
|
||||
|
||||
public NotificationProvidersController(
|
||||
ILogger<NotificationProvidersController> logger,
|
||||
DataContext dataContext,
|
||||
INotificationConfigurationService notificationConfigurationService,
|
||||
NotificationService notificationService)
|
||||
NotificationService notificationService,
|
||||
IAppriseCliDetector appriseCliDetector)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataContext = dataContext;
|
||||
_notificationConfigurationService = notificationConfigurationService;
|
||||
_notificationService = notificationService;
|
||||
_appriseCliDetector = appriseCliDetector;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -86,6 +89,18 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("apprise/cli-status")]
|
||||
public async Task<IActionResult> GetAppriseCliStatus()
|
||||
{
|
||||
string? version = await _appriseCliDetector.GetAppriseVersionAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Available = version is not null,
|
||||
Version = version
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("notifiarr")]
|
||||
public async Task<IActionResult> CreateNotifiarrProvider([FromBody] CreateNotifiarrProviderRequest newProvider)
|
||||
{
|
||||
@@ -162,9 +177,11 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
|
||||
var appriseConfig = new AppriseConfig
|
||||
{
|
||||
Mode = newProvider.Mode,
|
||||
Url = newProvider.Url,
|
||||
Key = newProvider.Key,
|
||||
Tags = newProvider.Tags
|
||||
Tags = newProvider.Tags,
|
||||
ServiceUrls = newProvider.ServiceUrls
|
||||
};
|
||||
appriseConfig.Validate();
|
||||
|
||||
@@ -382,9 +399,11 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
|
||||
var appriseConfig = new AppriseConfig
|
||||
{
|
||||
Mode = updatedProvider.Mode,
|
||||
Url = updatedProvider.Url,
|
||||
Key = updatedProvider.Key,
|
||||
Tags = updatedProvider.Tags
|
||||
Tags = updatedProvider.Tags,
|
||||
ServiceUrls = updatedProvider.ServiceUrls
|
||||
};
|
||||
|
||||
if (existingProvider.AppriseConfiguration != null)
|
||||
@@ -602,9 +621,11 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
{
|
||||
var appriseConfig = new AppriseConfig
|
||||
{
|
||||
Mode = testRequest.Mode,
|
||||
Url = testRequest.Url,
|
||||
Key = testRequest.Key,
|
||||
Tags = testRequest.Tags
|
||||
Tags = testRequest.Tags,
|
||||
ServiceUrls = testRequest.ServiceUrls
|
||||
};
|
||||
appriseConfig.Validate();
|
||||
|
||||
@@ -629,6 +650,10 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
}
|
||||
catch (AppriseException exception)
|
||||
{
|
||||
return StatusCode((int)HttpStatusCode.InternalServerError, exception.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Apprise provider");
|
||||
|
||||
7
code/backend/Cleanuparr.Domain/Enums/AppriseMode.cs
Normal file
7
code/backend/Cleanuparr.Domain/Enums/AppriseMode.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Cleanuparr.Domain.Enums;
|
||||
|
||||
public enum AppriseMode
|
||||
{
|
||||
Api,
|
||||
Cli
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.Notifications.Apprise;
|
||||
|
||||
public class AppriseCliDetectorTests
|
||||
{
|
||||
private readonly AppriseCliDetector _detector;
|
||||
|
||||
public AppriseCliDetectorTests()
|
||||
{
|
||||
_detector = new AppriseCliDetector(Substitute.For<ILogger<AppriseCliDetector>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CreatesInstance()
|
||||
{
|
||||
// Act
|
||||
var detector = new AppriseCliDetector(Substitute.For<ILogger<AppriseCliDetector>>());
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(detector);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAppriseVersionAsync_DoesNotThrow()
|
||||
{
|
||||
// Act & Assert - should handle missing CLI gracefully without throwing
|
||||
var exception = await Record.ExceptionAsync(() => _detector.GetAppriseVersionAsync());
|
||||
Assert.Null(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Xunit;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Tests.Features.Notifications.Apprise;
|
||||
|
||||
public class AppriseCliProxyTests
|
||||
{
|
||||
private readonly AppriseCliProxy _proxy;
|
||||
|
||||
public AppriseCliProxyTests()
|
||||
{
|
||||
_proxy = new AppriseCliProxy();
|
||||
}
|
||||
|
||||
private static ApprisePayload CreatePayload(string title = "Test Title", string body = "Test Body")
|
||||
{
|
||||
return new ApprisePayload
|
||||
{
|
||||
Title = title,
|
||||
Body = body,
|
||||
Type = "info"
|
||||
};
|
||||
}
|
||||
|
||||
private static AppriseConfig CreateConfig(string? serviceUrls = null)
|
||||
{
|
||||
return new AppriseConfig
|
||||
{
|
||||
ServiceUrls = serviceUrls
|
||||
};
|
||||
}
|
||||
|
||||
#region SendNotification Validation Tests
|
||||
|
||||
[Fact]
|
||||
public async Task SendNotification_WhenServiceUrlsIsNull_ThrowsAppriseException()
|
||||
{
|
||||
// Arrange
|
||||
var config = CreateConfig(null);
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<AppriseException>(() =>
|
||||
_proxy.SendNotification(CreatePayload(), config));
|
||||
Assert.Contains("No service URLs configured", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendNotification_WhenServiceUrlsIsEmpty_ThrowsAppriseException()
|
||||
{
|
||||
// Arrange
|
||||
var config = CreateConfig("");
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<AppriseException>(() =>
|
||||
_proxy.SendNotification(CreatePayload(), config));
|
||||
Assert.Contains("No service URLs configured", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendNotification_WhenServiceUrlsIsWhitespace_ThrowsAppriseException()
|
||||
{
|
||||
// Arrange
|
||||
var config = CreateConfig(" \n \n ");
|
||||
|
||||
// Act & Assert
|
||||
var ex = await Assert.ThrowsAsync<AppriseException>(() =>
|
||||
_proxy.SendNotification(CreatePayload(), config));
|
||||
Assert.Contains("No service URLs configured", ex.Message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -9,16 +9,19 @@ namespace Cleanuparr.Infrastructure.Tests.Features.Notifications;
|
||||
|
||||
public class AppriseProviderTests
|
||||
{
|
||||
private readonly Mock<IAppriseProxy> _proxyMock;
|
||||
private readonly Mock<IAppriseProxy> _apiProxyMock;
|
||||
private readonly Mock<IAppriseCliProxy> _cliProxyMock;
|
||||
private readonly AppriseConfig _config;
|
||||
private readonly AppriseProvider _provider;
|
||||
|
||||
public AppriseProviderTests()
|
||||
{
|
||||
_proxyMock = new Mock<IAppriseProxy>();
|
||||
_apiProxyMock = new Mock<IAppriseProxy>();
|
||||
_cliProxyMock = new Mock<IAppriseCliProxy>();
|
||||
_config = new AppriseConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Mode = AppriseMode.Api,
|
||||
Url = "http://apprise.example.com",
|
||||
Key = "testkey",
|
||||
Tags = "tag1,tag2"
|
||||
@@ -28,7 +31,8 @@ public class AppriseProviderTests
|
||||
"TestApprise",
|
||||
NotificationProviderType.Apprise,
|
||||
_config,
|
||||
_proxyMock.Object);
|
||||
_apiProxyMock.Object,
|
||||
_cliProxyMock.Object);
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
@@ -58,7 +62,7 @@ public class AppriseProviderTests
|
||||
var context = CreateTestContext();
|
||||
ApprisePayload? capturedPayload = null;
|
||||
|
||||
_proxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
_apiProxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
.Callback<ApprisePayload, AppriseConfig>((payload, config) => capturedPayload = payload)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
@@ -81,7 +85,7 @@ public class AppriseProviderTests
|
||||
|
||||
ApprisePayload? capturedPayload = null;
|
||||
|
||||
_proxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
_apiProxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
.Callback<ApprisePayload, AppriseConfig>((payload, config) => capturedPayload = payload)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
@@ -112,7 +116,7 @@ public class AppriseProviderTests
|
||||
|
||||
ApprisePayload? capturedPayload = null;
|
||||
|
||||
_proxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
_apiProxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
.Callback<ApprisePayload, AppriseConfig>((payload, config) => capturedPayload = payload)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
@@ -131,7 +135,7 @@ public class AppriseProviderTests
|
||||
var context = CreateTestContext();
|
||||
ApprisePayload? capturedPayload = null;
|
||||
|
||||
_proxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
_apiProxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
.Callback<ApprisePayload, AppriseConfig>((payload, config) => capturedPayload = payload)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
@@ -149,7 +153,7 @@ public class AppriseProviderTests
|
||||
// Arrange
|
||||
var context = CreateTestContext();
|
||||
|
||||
_proxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
_apiProxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
.ThrowsAsync(new Exception("Proxy error"));
|
||||
|
||||
// Act & Assert
|
||||
@@ -171,7 +175,7 @@ public class AppriseProviderTests
|
||||
|
||||
ApprisePayload? capturedPayload = null;
|
||||
|
||||
_proxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
_apiProxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), _config))
|
||||
.Callback<ApprisePayload, AppriseConfig>((payload, config) => capturedPayload = payload)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
@@ -183,6 +187,40 @@ public class AppriseProviderTests
|
||||
Assert.Contains("Test Description", capturedPayload.Body);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendNotificationAsync_CliMode_CallsCliProxy()
|
||||
{
|
||||
// Arrange
|
||||
var cliConfig = new AppriseConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Mode = AppriseMode.Cli,
|
||||
ServiceUrls = "discord://webhook_id/token"
|
||||
};
|
||||
|
||||
var apiProxyMock = new Mock<IAppriseProxy>();
|
||||
var cliProxyMock = new Mock<IAppriseCliProxy>();
|
||||
|
||||
var provider = new AppriseProvider(
|
||||
"TestAppriseCli",
|
||||
NotificationProviderType.Apprise,
|
||||
cliConfig,
|
||||
apiProxyMock.Object,
|
||||
cliProxyMock.Object);
|
||||
|
||||
var context = CreateTestContext();
|
||||
|
||||
cliProxyMock.Setup(p => p.SendNotification(It.IsAny<ApprisePayload>(), cliConfig))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await provider.SendNotificationAsync(context);
|
||||
|
||||
// Assert
|
||||
cliProxyMock.Verify(p => p.SendNotification(It.IsAny<ApprisePayload>(), cliConfig), Times.Once);
|
||||
apiProxyMock.Verify(p => p.SendNotification(It.IsAny<ApprisePayload>(), It.IsAny<AppriseConfig>()), Times.Never);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Cleanuparr.Infrastructure.Tests.Features.Notifications;
|
||||
public class NotificationProviderFactoryTests
|
||||
{
|
||||
private readonly Mock<IAppriseProxy> _appriseProxyMock;
|
||||
private readonly Mock<IAppriseCliProxy> _appriseCliProxyMock;
|
||||
private readonly Mock<INtfyProxy> _ntfyProxyMock;
|
||||
private readonly Mock<INotifiarrProxy> _notifiarrProxyMock;
|
||||
private readonly Mock<IPushoverProxy> _pushoverProxyMock;
|
||||
@@ -24,12 +25,14 @@ public class NotificationProviderFactoryTests
|
||||
public NotificationProviderFactoryTests()
|
||||
{
|
||||
_appriseProxyMock = new Mock<IAppriseProxy>();
|
||||
_appriseCliProxyMock = new Mock<IAppriseCliProxy>();
|
||||
_ntfyProxyMock = new Mock<INtfyProxy>();
|
||||
_notifiarrProxyMock = new Mock<INotifiarrProxy>();
|
||||
_pushoverProxyMock = new Mock<IPushoverProxy>();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_appriseProxyMock.Object);
|
||||
services.AddSingleton(_appriseCliProxyMock.Object);
|
||||
services.AddSingleton(_ntfyProxyMock.Object);
|
||||
services.AddSingleton(_notifiarrProxyMock.Object);
|
||||
services.AddSingleton(_pushoverProxyMock.Object);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CliWrap" Version="3.10.0" />
|
||||
<PackageReference Include="FLM.QBittorrent" Version="1.0.2" />
|
||||
<PackageReference Include="FLM.Transmission" Version="1.0.3" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Text;
|
||||
using CliWrap;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
|
||||
public sealed class AppriseCliDetector : IAppriseCliDetector
|
||||
{
|
||||
private readonly ILogger<AppriseCliDetector> _logger;
|
||||
|
||||
private static readonly TimeSpan DetectionTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
public AppriseCliDetector(ILogger<AppriseCliDetector> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<string?> GetAppriseVersionAsync()
|
||||
{
|
||||
using var cts = new CancellationTokenSource(DetectionTimeout);
|
||||
|
||||
try
|
||||
{
|
||||
StringBuilder version = new();
|
||||
_ = await Cli.Wrap("apprise")
|
||||
.WithArguments("--version")
|
||||
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(version))
|
||||
.ExecuteAsync(cts.Token);
|
||||
|
||||
return version.ToString().Split('\n').FirstOrDefault();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to get apprise version");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Text;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using CliWrap;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
|
||||
public sealed class AppriseCliProxy : IAppriseCliProxy
|
||||
{
|
||||
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
public async Task SendNotification(ApprisePayload payload, AppriseConfig config)
|
||||
{
|
||||
var serviceUrls = config.ServiceUrls?
|
||||
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(u => u.Trim())
|
||||
.Where(u => !string.IsNullOrEmpty(u))
|
||||
.ToArray();
|
||||
|
||||
if (serviceUrls == null || serviceUrls.Length == 0)
|
||||
{
|
||||
throw new AppriseException("No service URLs configured");
|
||||
}
|
||||
|
||||
var args = new List<string> { "--verbose" };
|
||||
|
||||
if (!string.IsNullOrEmpty(payload.Title))
|
||||
{
|
||||
args.AddRange(["--title", payload.Title]);
|
||||
}
|
||||
|
||||
args.AddRange(["--body", payload.Body, "--notification-type", payload.Type]);
|
||||
args.AddRange(serviceUrls);
|
||||
|
||||
await ExecuteAppriseAsync(args);
|
||||
}
|
||||
|
||||
private static async Task ExecuteAppriseAsync(IEnumerable<string> arguments)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(DefaultTimeout);
|
||||
StringBuilder message = new();
|
||||
|
||||
try
|
||||
{
|
||||
CommandResult result = await Cli.Wrap("apprise")
|
||||
.WithArguments(arguments)
|
||||
.WithStandardOutputPipe(PipeTarget.ToStringBuilder(message))
|
||||
.WithStandardErrorPipe(PipeTarget.ToStringBuilder(message))
|
||||
.WithValidation(CommandResultValidation.None)
|
||||
.ExecuteAsync(cts.Token);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new AppriseException($"Apprise CLI failed with: {message}");
|
||||
}
|
||||
}
|
||||
catch (AppriseException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw new AppriseException($"Apprise CLI timed out after {DefaultTimeout.TotalSeconds} seconds.");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
throw new AppriseException("Apprise CLI failed", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
using Cleanuparr.Infrastructure.Features.Notifications.Models;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using System.Text;
|
||||
@@ -8,22 +7,33 @@ namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
|
||||
public sealed class AppriseProvider : NotificationProviderBase<AppriseConfig>
|
||||
{
|
||||
private readonly IAppriseProxy _proxy;
|
||||
private readonly IAppriseProxy _apiProxy;
|
||||
private readonly IAppriseCliProxy _cliProxy;
|
||||
|
||||
public AppriseProvider(
|
||||
string name,
|
||||
NotificationProviderType type,
|
||||
AppriseConfig config,
|
||||
IAppriseProxy proxy
|
||||
IAppriseProxy apiProxy,
|
||||
IAppriseCliProxy cliProxy
|
||||
) : base(name, type, config)
|
||||
{
|
||||
_proxy = proxy;
|
||||
_apiProxy = apiProxy;
|
||||
_cliProxy = cliProxy;
|
||||
}
|
||||
|
||||
public override async Task SendNotificationAsync(NotificationContext context)
|
||||
{
|
||||
ApprisePayload payload = BuildPayload(context);
|
||||
await _proxy.SendNotification(payload, Config);
|
||||
|
||||
if (Config.Mode is AppriseMode.Cli)
|
||||
{
|
||||
await _cliProxy.SendNotification(payload, Config);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _apiProxy.SendNotification(payload, Config);
|
||||
}
|
||||
}
|
||||
|
||||
private ApprisePayload BuildPayload(NotificationContext context)
|
||||
@@ -51,7 +61,7 @@ public sealed class AppriseProvider : NotificationProviderBase<AppriseConfig>
|
||||
var body = new StringBuilder();
|
||||
body.AppendLine(context.Description);
|
||||
body.AppendLine();
|
||||
|
||||
|
||||
foreach ((string key, string value) in context.Data)
|
||||
{
|
||||
body.AppendLine($"{key}: {value}");
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
|
||||
public interface IAppriseCliDetector
|
||||
{
|
||||
Task<string?> GetAppriseVersionAsync();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise;
|
||||
|
||||
public interface IAppriseCliProxy
|
||||
{
|
||||
Task SendNotification(ApprisePayload payload, AppriseConfig config);
|
||||
}
|
||||
@@ -41,9 +41,10 @@ public sealed class NotificationProviderFactory : INotificationProviderFactory
|
||||
private INotificationProvider CreateAppriseProvider(NotificationProviderDto config)
|
||||
{
|
||||
var appriseConfig = (AppriseConfig)config.Configuration;
|
||||
var proxy = _serviceProvider.GetRequiredService<IAppriseProxy>();
|
||||
|
||||
return new AppriseProvider(config.Name, config.Type, appriseConfig, proxy);
|
||||
var apiProxy = _serviceProvider.GetRequiredService<IAppriseProxy>();
|
||||
var cliProxy = _serviceProvider.GetRequiredService<IAppriseCliProxy>();
|
||||
|
||||
return new AppriseProvider(config.Name, config.Type, appriseConfig, apiProxy, cliProxy);
|
||||
}
|
||||
|
||||
private INotificationProvider CreateNtfyProvider(NotificationProviderDto config)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Notification;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
@@ -142,7 +143,7 @@ public sealed class AppriseConfigTests
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
exception.Message.ShouldBe("Apprise server URL is required");
|
||||
exception.Message.ShouldBe("Apprise server URL is required for API mode");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -171,7 +172,7 @@ public sealed class AppriseConfigTests
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
exception.Message.ShouldBe("Apprise configuration key is required");
|
||||
exception.Message.ShouldBe("Apprise configuration key is required for API mode");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -200,4 +201,104 @@ public sealed class AppriseConfigTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CLI Mode Tests
|
||||
|
||||
[Fact]
|
||||
public void IsValid_CliMode_WithValidServiceUrls_ReturnsTrue()
|
||||
{
|
||||
var config = new AppriseConfig
|
||||
{
|
||||
Mode = AppriseMode.Cli,
|
||||
ServiceUrls = "discord://webhook_id/webhook_token"
|
||||
};
|
||||
|
||||
config.IsValid().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void IsValid_CliMode_WithEmptyServiceUrls_ReturnsFalse(string? serviceUrls)
|
||||
{
|
||||
var config = new AppriseConfig
|
||||
{
|
||||
Mode = AppriseMode.Cli,
|
||||
ServiceUrls = serviceUrls
|
||||
};
|
||||
|
||||
config.IsValid().ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CliMode_WithValidServiceUrls_DoesNotThrow()
|
||||
{
|
||||
var config = new AppriseConfig
|
||||
{
|
||||
Mode = AppriseMode.Cli,
|
||||
ServiceUrls = "discord://webhook_id/webhook_token\nslack://token_a/token_b/token_c"
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void Validate_CliMode_WithEmptyServiceUrls_ThrowsValidationException(string? serviceUrls)
|
||||
{
|
||||
var config = new AppriseConfig
|
||||
{
|
||||
Mode = AppriseMode.Cli,
|
||||
ServiceUrls = serviceUrls
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
exception.Message.ShouldBe("At least one service URL is required for CLI mode");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_CliMode_WithValidUrlAndWhitespaceLines_DoesNotThrow()
|
||||
{
|
||||
// url1 is valid content, whitespace lines should be filtered out
|
||||
var config = new AppriseConfig
|
||||
{
|
||||
Mode = AppriseMode.Cli,
|
||||
ServiceUrls = "discord://webhook_id/token\n \n "
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_CliMode_IgnoresApiModeFields()
|
||||
{
|
||||
var config = new AppriseConfig
|
||||
{
|
||||
Mode = AppriseMode.Cli,
|
||||
Url = string.Empty, // Would be invalid in API mode
|
||||
Key = string.Empty, // Would be invalid in API mode
|
||||
ServiceUrls = "discord://webhook_id/webhook_token"
|
||||
};
|
||||
|
||||
config.IsValid().ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValid_ApiMode_IgnoresCliModeFields()
|
||||
{
|
||||
var config = new AppriseConfig
|
||||
{
|
||||
Mode = AppriseMode.Api,
|
||||
Url = "https://apprise.example.com",
|
||||
Key = "my-key",
|
||||
ServiceUrls = null // Would be invalid in CLI mode
|
||||
};
|
||||
|
||||
config.IsValid().ShouldBeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
1101
code/backend/Cleanuparr.Persistence/Migrations/Data/20251213201344_AddAppriseCliMode.Designer.cs
generated
Normal file
1101
code/backend/Cleanuparr.Persistence/Migrations/Data/20251213201344_AddAppriseCliMode.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAppriseCliMode : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "mode",
|
||||
table: "apprise_configs",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "api");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "service_urls",
|
||||
table: "apprise_configs",
|
||||
type: "TEXT",
|
||||
maxLength: 4000,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "mode",
|
||||
table: "apprise_configs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "service_urls",
|
||||
table: "apprise_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,10 +479,20 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<string>("Mode")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("mode");
|
||||
|
||||
b.Property<Guid>("NotificationConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("notification_config_id");
|
||||
|
||||
b.Property<string>("ServiceUrls")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("service_urls");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
|
||||
|
||||
@@ -10,23 +12,36 @@ public sealed record AppriseConfig : IConfig
|
||||
[Key]
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
|
||||
|
||||
[Required]
|
||||
[ExcludeFromCodeCoverage]
|
||||
public Guid NotificationConfigId { get; init; }
|
||||
|
||||
|
||||
public NotificationConfig NotificationConfig { get; init; } = null!;
|
||||
|
||||
[Required]
|
||||
|
||||
/// <summary>
|
||||
/// The mode of operation: Api (external apprise-api container) or Cli (bundled apprise CLI)
|
||||
/// </summary>
|
||||
public AppriseMode Mode { get; init; } = AppriseMode.Api;
|
||||
|
||||
// API mode fields
|
||||
[MaxLength(500)]
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
|
||||
[MaxLength(255)]
|
||||
public string Key { get; init; } = string.Empty;
|
||||
|
||||
|
||||
[MaxLength(255)]
|
||||
public string? Tags { get; init; }
|
||||
|
||||
|
||||
// CLI mode fields
|
||||
/// <summary>
|
||||
/// Apprise service URLs for CLI mode (one per line).
|
||||
/// Example: discord://webhook_id/webhook_token
|
||||
/// </summary>
|
||||
[MaxLength(4000)]
|
||||
public string? ServiceUrls { get; init; }
|
||||
|
||||
[NotMapped]
|
||||
public Uri? Uri
|
||||
{
|
||||
@@ -42,33 +57,56 @@ public sealed record AppriseConfig : IConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return Uri != null &&
|
||||
!string.IsNullOrWhiteSpace(Key);
|
||||
return Mode switch
|
||||
{
|
||||
AppriseMode.Api => Uri != null && !string.IsNullOrWhiteSpace(Key),
|
||||
AppriseMode.Cli => !string.IsNullOrWhiteSpace(ServiceUrls),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (Mode is AppriseMode.Api)
|
||||
{
|
||||
ValidateApiMode();
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateCliMode();
|
||||
}
|
||||
|
||||
private void ValidateApiMode()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Url))
|
||||
{
|
||||
throw new ValidationException("Apprise server URL is required");
|
||||
throw new ValidationException("Apprise server URL is required for API mode");
|
||||
}
|
||||
|
||||
if (Uri == null)
|
||||
|
||||
if (Uri is null)
|
||||
{
|
||||
throw new ValidationException("Apprise server URL must be a valid HTTP or HTTPS URL");
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Key))
|
||||
{
|
||||
throw new ValidationException("Apprise configuration key is required");
|
||||
throw new ValidationException("Apprise configuration key is required for API mode");
|
||||
}
|
||||
|
||||
|
||||
if (Key.Length < 2)
|
||||
{
|
||||
throw new ValidationException("Apprise configuration key must be at least 2 characters long");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateCliMode()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ServiceUrls))
|
||||
{
|
||||
throw new ValidationException("At least one service URL is required for CLI mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +119,11 @@ export class DocumentationService {
|
||||
'notifiarr.channelId': 'channel-id'
|
||||
},
|
||||
'notifications/apprise': {
|
||||
'apprise.mode': 'mode',
|
||||
'apprise.url': 'url',
|
||||
'apprise.key': 'key',
|
||||
'apprise.tags': 'tags'
|
||||
'apprise.tags': 'tags',
|
||||
'apprise.serviceUrls': 'service-urls'
|
||||
},
|
||||
'notifications/ntfy': {
|
||||
'ntfy.serverUrl': 'server-url',
|
||||
|
||||
@@ -7,11 +7,16 @@ import {
|
||||
NotificationProviderDto,
|
||||
TestNotificationResult
|
||||
} from '../../shared/models/notification-provider.model';
|
||||
import { NotificationProviderType } from '../../shared/models/enums';
|
||||
import { AppriseMode, NotificationProviderType } from '../../shared/models/enums';
|
||||
import { NtfyAuthenticationType } from '../../shared/models/ntfy-authentication-type.enum';
|
||||
import { NtfyPriority } from '../../shared/models/ntfy-priority.enum';
|
||||
import { PushoverPriority } from '../../shared/models/pushover-priority.enum';
|
||||
|
||||
export interface AppriseCliStatus {
|
||||
available: boolean;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
// Provider-specific interfaces
|
||||
export interface CreateNotifiarrProviderRequest {
|
||||
name: string;
|
||||
@@ -53,9 +58,13 @@ export interface CreateAppriseProviderRequest {
|
||||
onQueueItemDeleted: boolean;
|
||||
onDownloadCleaned: boolean;
|
||||
onCategoryChanged: boolean;
|
||||
mode: AppriseMode;
|
||||
// API mode fields
|
||||
url: string;
|
||||
key: string;
|
||||
tags: string;
|
||||
// CLI mode fields
|
||||
serviceUrls: string;
|
||||
}
|
||||
|
||||
export interface UpdateAppriseProviderRequest {
|
||||
@@ -67,15 +76,23 @@ export interface UpdateAppriseProviderRequest {
|
||||
onQueueItemDeleted: boolean;
|
||||
onDownloadCleaned: boolean;
|
||||
onCategoryChanged: boolean;
|
||||
mode: AppriseMode;
|
||||
// API mode fields
|
||||
url: string;
|
||||
key: string;
|
||||
tags: string;
|
||||
// CLI mode fields
|
||||
serviceUrls: string;
|
||||
}
|
||||
|
||||
export interface TestAppriseProviderRequest {
|
||||
mode: AppriseMode;
|
||||
// API mode fields
|
||||
url: string;
|
||||
key: string;
|
||||
tags: string;
|
||||
// CLI mode fields
|
||||
serviceUrls: string;
|
||||
}
|
||||
|
||||
export interface CreateNtfyProviderRequest {
|
||||
@@ -191,6 +208,13 @@ export class NotificationProviderService {
|
||||
return this.http.get<NotificationProvidersConfig>(this.baseUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Apprise CLI availability status
|
||||
*/
|
||||
getAppriseCliStatus(): Observable<AppriseCliStatus> {
|
||||
return this.http.get<AppriseCliStatus>(`${this.baseUrl}/apprise/cli-status`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Notifiarr provider
|
||||
*/
|
||||
|
||||
@@ -10,67 +10,145 @@
|
||||
>
|
||||
<!-- Provider-specific configuration goes here -->
|
||||
<div slot="provider-config">
|
||||
<!-- Apprise Server URL -->
|
||||
<!-- Mode Selection -->
|
||||
<div class="field">
|
||||
<label for="full-url">
|
||||
<label for="mode">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('apprise.url')"
|
||||
(click)="openFieldDocs('apprise.mode')"
|
||||
></i>
|
||||
Apprise Server URL *
|
||||
Mode *
|
||||
</label>
|
||||
<input
|
||||
id="full-url"
|
||||
type="url"
|
||||
pInputText
|
||||
[formControl]="urlControl"
|
||||
placeholder="http://localhost:8000"
|
||||
<p-select
|
||||
id="mode"
|
||||
[options]="modeOptions"
|
||||
[formControl]="modeControl"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasFieldError(urlControl, 'required')" class="form-error-text">URL is required</small>
|
||||
<small *ngIf="hasFieldError(urlControl, 'invalidUri')" class="form-error-text">Must be a valid URL</small>
|
||||
<small *ngIf="hasFieldError(urlControl, 'invalidProtocol')" class="form-error-text">Must use http or https protocol</small>
|
||||
<small class="form-helper-text">The URL of your Apprise server where notifications will be sent.</small>
|
||||
[showClear]="false"
|
||||
></p-select>
|
||||
<small class="form-helper-text">
|
||||
API mode requires an external Apprise container. CLI mode uses the Apprise CLI directly, but requires installation for non-Docker users.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Key -->
|
||||
<div class="field">
|
||||
<label for="key">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('apprise.key')"
|
||||
></i>
|
||||
Configuration Key *
|
||||
</label>
|
||||
<input id="key" type="text" pInputText [formControl]="keyControl" placeholder="my-config-key" class="w-full" />
|
||||
<small *ngIf="hasFieldError(keyControl, 'required')" class="form-error-text">Configuration key is required</small>
|
||||
<small *ngIf="hasFieldError(keyControl, 'minlength')" class="form-error-text">Key must be at least 2 characters</small>
|
||||
<small class="form-helper-text">The key that identifies your Apprise configuration on the server.</small>
|
||||
<!-- CLI Availability Loading -->
|
||||
<div *ngIf="modeControl.value === 'Cli' && checkingCliAvailability" class="cli-detection-loading mb-3">
|
||||
<p-progressSpinner styleClass="w-1rem h-1rem"></p-progressSpinner>
|
||||
<span>Detecting local Apprise version...</span>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="field">
|
||||
<label for="tags">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('apprise.tags')"
|
||||
></i>
|
||||
Tags (Optional)
|
||||
</label>
|
||||
<input
|
||||
id="tags"
|
||||
type="text"
|
||||
pInputText
|
||||
[formControl]="tagsControl"
|
||||
placeholder="tag1,tag2 or tag3 tag4"
|
||||
class="w-full"
|
||||
/>
|
||||
<small class="form-helper-text"
|
||||
>Optional tags to filter notifications. Use comma (,) to OR tags and space ( ) to AND them.</small
|
||||
>
|
||||
</div>
|
||||
<!-- CLI Availability Warning -->
|
||||
<p-message
|
||||
*ngIf="modeControl.value === 'Cli' && !checkingCliAvailability && !cliAvailable"
|
||||
severity="warn"
|
||||
styleClass="w-full mb-3"
|
||||
>
|
||||
Apprise CLI not detected.
|
||||
<a href="https://github.com/caronc/apprise#installation" target="_blank" rel="noopener">
|
||||
Installation Guide
|
||||
</a>
|
||||
</p-message>
|
||||
|
||||
<!-- CLI Available Info -->
|
||||
<p-message
|
||||
*ngIf="modeControl.value === 'Cli' && !checkingCliAvailability && cliAvailable && cliVersion"
|
||||
severity="success"
|
||||
styleClass="w-full mb-3"
|
||||
[text]="'Apprise CLI detected: ' + cliVersion"
|
||||
></p-message>
|
||||
|
||||
<!-- API Mode Fields -->
|
||||
<ng-container *ngIf="modeControl.value === 'Api'">
|
||||
<!-- Apprise Server URL -->
|
||||
<div class="field">
|
||||
<label for="full-url">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('apprise.url')"
|
||||
></i>
|
||||
Apprise Server URL *
|
||||
</label>
|
||||
<input
|
||||
id="full-url"
|
||||
type="url"
|
||||
pInputText
|
||||
[formControl]="urlControl"
|
||||
placeholder="http://localhost:8000"
|
||||
class="w-full"
|
||||
/>
|
||||
<small *ngIf="hasFieldError(urlControl, 'required')" class="form-error-text">URL is required</small>
|
||||
<small *ngIf="hasFieldError(urlControl, 'invalidUri')" class="form-error-text">Must be a valid URL</small>
|
||||
<small *ngIf="hasFieldError(urlControl, 'invalidProtocol')" class="form-error-text">Must use http or https protocol</small>
|
||||
<small class="form-helper-text">The URL of your Apprise server where notifications will be sent.</small>
|
||||
</div>
|
||||
|
||||
<!-- Configuration Key -->
|
||||
<div class="field">
|
||||
<label for="key">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('apprise.key')"
|
||||
></i>
|
||||
Configuration Key *
|
||||
</label>
|
||||
<input id="key" type="text" pInputText [formControl]="keyControl" placeholder="my-config-key" class="w-full" />
|
||||
<small *ngIf="hasFieldError(keyControl, 'required')" class="form-error-text">Configuration key is required</small>
|
||||
<small *ngIf="hasFieldError(keyControl, 'minlength')" class="form-error-text">Key must be at least 2 characters</small>
|
||||
<small class="form-helper-text">The key that identifies your Apprise configuration on the server.</small>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="field">
|
||||
<label for="tags">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('apprise.tags')"
|
||||
></i>
|
||||
Tags (Optional)
|
||||
</label>
|
||||
<input
|
||||
id="tags"
|
||||
type="text"
|
||||
pInputText
|
||||
[formControl]="tagsControl"
|
||||
placeholder="tag1,tag2 or tag3 tag4"
|
||||
class="w-full"
|
||||
/>
|
||||
<small class="form-helper-text"
|
||||
>Optional tags to filter notifications. Use comma (,) to OR tags and space ( ) to AND them.</small
|
||||
>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- CLI Mode Fields -->
|
||||
<ng-container *ngIf="modeControl.value === 'Cli'">
|
||||
<!-- Service URLs -->
|
||||
<div class="field">
|
||||
<label for="serviceUrls">
|
||||
<i
|
||||
class="pi pi-question-circle field-info-icon"
|
||||
title="Click for documentation"
|
||||
(click)="openFieldDocs('apprise.serviceUrls')"
|
||||
></i>
|
||||
Service URLs *
|
||||
</label>
|
||||
<app-mobile-autocomplete
|
||||
[formControl]="serviceUrlsControl"
|
||||
placeholder="Add service URL and press Enter"
|
||||
></app-mobile-autocomplete>
|
||||
<small class="form-helper-text">
|
||||
Add Apprise service URLs. Example: discord://webhook_id/token.
|
||||
<a href="https://github.com/caronc/apprise/wiki#notification-services" target="_blank" rel="noopener">
|
||||
View all supported services
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</app-notification-provider-base>
|
||||
|
||||
@@ -1,2 +1,10 @@
|
||||
/* Apprise Provider Modal Styles */
|
||||
@use '../../../styles/settings-shared.scss';
|
||||
|
||||
.cli-detection-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, inject } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
||||
import { FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { InputTextModule } from 'primeng/inputtext';
|
||||
import { MobileAutocompleteComponent } from '../../../../shared/components/mobile-autocomplete/mobile-autocomplete.component';
|
||||
import { SelectModule } from 'primeng/select';
|
||||
import { Message } from 'primeng/message';
|
||||
import { ProgressSpinnerModule } from 'primeng/progressspinner';
|
||||
import { AppriseFormData, BaseProviderFormData } from '../../models/provider-modal.model';
|
||||
import { DocumentationService } from '../../../../core/services/documentation.service';
|
||||
import { NotificationProviderService } from '../../../../core/services/notification-provider.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 { AppriseMode } from '../../../../shared/models/enums';
|
||||
|
||||
@Component({
|
||||
selector: 'app-apprise-provider',
|
||||
@@ -15,12 +22,16 @@ import { UrlValidators } from '../../../../core/validators/url.validator';
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
InputTextModule,
|
||||
SelectModule,
|
||||
Message,
|
||||
ProgressSpinnerModule,
|
||||
MobileAutocompleteComponent,
|
||||
NotificationProviderBaseComponent
|
||||
],
|
||||
templateUrl: './apprise-provider.component.html',
|
||||
styleUrls: ['./apprise-provider.component.scss']
|
||||
})
|
||||
export class AppriseProviderComponent implements OnInit, OnChanges {
|
||||
export class AppriseProviderComponent implements OnInit, OnChanges, OnDestroy {
|
||||
@Input() visible = false;
|
||||
@Input() editingProvider: NotificationProviderDto | null = null;
|
||||
@Input() saving = false;
|
||||
@@ -30,11 +41,32 @@ export class AppriseProviderComponent implements OnInit, OnChanges {
|
||||
@Output() cancel = new EventEmitter<void>();
|
||||
@Output() test = new EventEmitter<AppriseFormData>();
|
||||
|
||||
// Provider-specific form controls
|
||||
private documentationService = inject(DocumentationService);
|
||||
private notificationProviderService = inject(NotificationProviderService);
|
||||
|
||||
// Mode selection
|
||||
modeControl = new FormControl<AppriseMode>(AppriseMode.Api, { nonNullable: true });
|
||||
modeOptions = [
|
||||
{ label: 'API', value: AppriseMode.Api },
|
||||
{ label: 'CLI', value: AppriseMode.Cli }
|
||||
];
|
||||
|
||||
// CLI availability status
|
||||
checkingCliAvailability = false;
|
||||
cliAvailable = false;
|
||||
cliVersion: string | null = null;
|
||||
|
||||
// API mode form controls
|
||||
urlControl = new FormControl('', [Validators.required, UrlValidators.httpUrl]);
|
||||
keyControl = new FormControl('', [Validators.required, Validators.minLength(2)]);
|
||||
tagsControl = new FormControl(''); // Optional field
|
||||
private documentationService = inject(DocumentationService);
|
||||
|
||||
// CLI mode form controls
|
||||
serviceUrlsControl = new FormControl<string[]>([]);
|
||||
|
||||
// Subscription for mode changes
|
||||
private modeSubscription?: Subscription;
|
||||
private cliCheckedThisSession = false;
|
||||
|
||||
/**
|
||||
* Exposed for template to open documentation for apprise fields
|
||||
@@ -44,7 +76,16 @@ export class AppriseProviderComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Initialize component but don't populate yet - wait for ngOnChanges
|
||||
// Subscribe to mode changes to check CLI availability when switching to CLI mode
|
||||
this.modeSubscription = this.modeControl.valueChanges.subscribe((mode) => {
|
||||
if (mode === AppriseMode.Cli && !this.cliCheckedThisSession) {
|
||||
this.checkCliAvailability();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.modeSubscription?.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
@@ -57,41 +98,106 @@ export class AppriseProviderComponent implements OnInit, OnChanges {
|
||||
this.resetProviderFields();
|
||||
}
|
||||
}
|
||||
|
||||
// When modal becomes visible, reset the CLI check flag and check if already in CLI mode
|
||||
if (changes['visible'] && this.visible) {
|
||||
this.cliCheckedThisSession = false;
|
||||
// Only check CLI availability if mode is already CLI (editing existing CLI provider)
|
||||
if (this.modeControl.value === AppriseMode.Cli) {
|
||||
this.checkCliAvailability();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCliAvailability(): void {
|
||||
this.checkingCliAvailability = true;
|
||||
this.cliCheckedThisSession = true;
|
||||
this.notificationProviderService.getAppriseCliStatus().subscribe({
|
||||
next: (status) => {
|
||||
this.cliAvailable = status.available;
|
||||
this.cliVersion = status.version;
|
||||
this.checkingCliAvailability = false;
|
||||
},
|
||||
error: () => {
|
||||
this.cliAvailable = false;
|
||||
this.cliVersion = null;
|
||||
this.checkingCliAvailability = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private populateProviderFields(): void {
|
||||
if (this.editingProvider) {
|
||||
const config = this.editingProvider.configuration as any;
|
||||
|
||||
|
||||
this.modeControl.setValue(config?.mode || AppriseMode.Api);
|
||||
// API mode fields
|
||||
this.urlControl.setValue(config?.url || '');
|
||||
this.keyControl.setValue(config?.key || '');
|
||||
this.tagsControl.setValue(config?.tags || '');
|
||||
// CLI mode fields - convert newline-separated string to array
|
||||
const serviceUrlsString = config?.serviceUrls || '';
|
||||
const serviceUrlsArray = serviceUrlsString
|
||||
.split('\n')
|
||||
.map((url: string) => url.trim())
|
||||
.filter((url: string) => url.length > 0);
|
||||
this.serviceUrlsControl.setValue(serviceUrlsArray);
|
||||
}
|
||||
}
|
||||
|
||||
private resetProviderFields(): void {
|
||||
this.modeControl.setValue(AppriseMode.Api);
|
||||
this.urlControl.setValue('');
|
||||
this.keyControl.setValue('');
|
||||
this.tagsControl.setValue('');
|
||||
this.serviceUrlsControl.setValue([]);
|
||||
}
|
||||
|
||||
protected hasFieldError(control: FormControl, errorType: string): boolean {
|
||||
return !!(control && control.errors?.[errorType] && (control.dirty || control.touched));
|
||||
}
|
||||
|
||||
onSave(baseData: BaseProviderFormData): void {
|
||||
if (this.urlControl.valid && this.keyControl.valid) {
|
||||
const appriseData: AppriseFormData = {
|
||||
...baseData,
|
||||
url: this.urlControl.value || '',
|
||||
key: this.keyControl.value || '',
|
||||
tags: this.tagsControl.value || ''
|
||||
};
|
||||
this.save.emit(appriseData);
|
||||
private isFormValid(): boolean {
|
||||
const mode = this.modeControl.value;
|
||||
if (mode === AppriseMode.Api) {
|
||||
return this.urlControl.valid && this.keyControl.valid;
|
||||
} else {
|
||||
// Mark provider-specific fields as touched to show validation errors
|
||||
// CLI mode requires at least one service URL
|
||||
const serviceUrls = this.serviceUrlsControl.value || [];
|
||||
return serviceUrls.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
private markFieldsTouched(): void {
|
||||
const mode = this.modeControl.value;
|
||||
if (mode === AppriseMode.Api) {
|
||||
this.urlControl.markAsTouched();
|
||||
this.keyControl.markAsTouched();
|
||||
} else {
|
||||
this.serviceUrlsControl.markAsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
private buildFormData(baseData: BaseProviderFormData): AppriseFormData {
|
||||
// Convert array to newline-separated string for backend
|
||||
const serviceUrlsArray = this.serviceUrlsControl.value || [];
|
||||
const serviceUrlsString = serviceUrlsArray.join('\n');
|
||||
|
||||
return {
|
||||
...baseData,
|
||||
mode: this.modeControl.value,
|
||||
url: this.urlControl.value || '',
|
||||
key: this.keyControl.value || '',
|
||||
tags: this.tagsControl.value || '',
|
||||
serviceUrls: serviceUrlsString
|
||||
};
|
||||
}
|
||||
|
||||
onSave(baseData: BaseProviderFormData): void {
|
||||
if (this.isFormValid()) {
|
||||
this.save.emit(this.buildFormData(baseData));
|
||||
} else {
|
||||
this.markFieldsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,20 +206,10 @@ export class AppriseProviderComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
onTest(baseData: BaseProviderFormData): void {
|
||||
if (this.urlControl.valid && this.keyControl.valid) {
|
||||
const appriseData: AppriseFormData = {
|
||||
...baseData,
|
||||
url: this.urlControl.value || '',
|
||||
key: this.keyControl.value || '',
|
||||
tags: this.tagsControl.value || ''
|
||||
};
|
||||
this.test.emit(appriseData);
|
||||
if (this.isFormValid()) {
|
||||
this.test.emit(this.buildFormData(baseData));
|
||||
} else {
|
||||
// Mark provider-specific fields as touched to show validation errors
|
||||
this.urlControl.markAsTouched();
|
||||
this.keyControl.markAsTouched();
|
||||
this.markFieldsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
// URL validation delegated to shared UrlValidators.httpUrl
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
[closable]="true"
|
||||
[draggable]="false"
|
||||
[resizable]="false"
|
||||
styleClass="instance-modal"
|
||||
styleClass="notification-provider-modal"
|
||||
[header]="modalTitle"
|
||||
(onHide)="onCancel()"
|
||||
>
|
||||
@@ -111,7 +111,7 @@
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<ng-template pTemplate="footer">
|
||||
<div>
|
||||
<div class="pt-3">
|
||||
<button pButton type="button" label="Cancel" class="p-button-text" (click)="onCancel()"></button>
|
||||
<button
|
||||
pButton
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
/* Base Notification Provider Modal Styles */
|
||||
@use '../../../styles/settings-shared.scss';
|
||||
|
||||
::ng-deep .notification-provider-modal.p-dialog {
|
||||
max-width: 600px !important;
|
||||
min-width: 320px !important;
|
||||
}
|
||||
@@ -44,26 +44,13 @@
|
||||
></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="form-error-text">At least one topic is required</small>
|
||||
<small *ngIf="hasFieldError(topicsControl, 'minlength')" class="form-error-text">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>
|
||||
<small class="form-helper-text">Enter the ntfy topics you want to publish to. Press Enter or click + to add each topic.</small>
|
||||
</div>
|
||||
|
||||
<!-- Authentication Type -->
|
||||
@@ -190,24 +177,11 @@
|
||||
></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>
|
||||
<small class="form-helper-text">Optional tags to add to notifications (e.g., warning, alert). Press Enter or click + to add each tag.</small>
|
||||
</div>
|
||||
</div>
|
||||
</app-notification-provider-base>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, inject } from '@angular/core';
|
||||
import { Component, Input, Output, EventEmitter, OnInit, 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';
|
||||
@@ -20,7 +19,6 @@ import { NtfyPriority } from '../../../../shared/models/ntfy-priority.enum';
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
InputTextModule,
|
||||
AutoCompleteModule,
|
||||
SelectModule,
|
||||
MobileAutocompleteComponent,
|
||||
NotificationProviderBaseComponent
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NotificationProviderType } from '../../../shared/models/enums';
|
||||
import { AppriseMode, NotificationProviderType } from '../../../shared/models/enums';
|
||||
import { NtfyAuthenticationType } from '../../../shared/models/ntfy-authentication-type.enum';
|
||||
import { NtfyPriority } from '../../../shared/models/ntfy-priority.enum';
|
||||
import { PushoverPriority } from '../../../shared/models/pushover-priority.enum';
|
||||
@@ -34,9 +34,13 @@ export interface NotifiarrFormData extends BaseProviderFormData {
|
||||
}
|
||||
|
||||
export interface AppriseFormData extends BaseProviderFormData {
|
||||
mode: AppriseMode;
|
||||
// API mode fields
|
||||
url: string;
|
||||
key: string;
|
||||
tags: string;
|
||||
// CLI mode fields
|
||||
serviceUrls: string;
|
||||
}
|
||||
|
||||
export interface NtfyFormData extends BaseProviderFormData {
|
||||
|
||||
@@ -281,9 +281,11 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
case NotificationProviderType.Apprise:
|
||||
const appriseConfig = provider.configuration as any;
|
||||
testRequest = {
|
||||
url: appriseConfig.url,
|
||||
key: appriseConfig.key,
|
||||
mode: appriseConfig.mode,
|
||||
url: appriseConfig.url || "",
|
||||
key: appriseConfig.key || "",
|
||||
tags: appriseConfig.tags || "",
|
||||
serviceUrls: appriseConfig.serviceUrls || "",
|
||||
};
|
||||
break;
|
||||
case NotificationProviderType.Ntfy:
|
||||
@@ -415,9 +417,11 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
*/
|
||||
onAppriseTest(data: AppriseFormData): void {
|
||||
const testRequest = {
|
||||
mode: data.mode,
|
||||
url: data.url,
|
||||
key: data.key,
|
||||
tags: data.tags,
|
||||
serviceUrls: data.serviceUrls,
|
||||
};
|
||||
|
||||
this.notificationProviderStore.testProvider({
|
||||
@@ -575,9 +579,11 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
onQueueItemDeleted: data.onQueueItemDeleted,
|
||||
onDownloadCleaned: data.onDownloadCleaned,
|
||||
onCategoryChanged: data.onCategoryChanged,
|
||||
mode: data.mode,
|
||||
url: data.url,
|
||||
key: data.key,
|
||||
tags: data.tags,
|
||||
serviceUrls: data.serviceUrls,
|
||||
};
|
||||
|
||||
this.notificationProviderStore.createProvider({
|
||||
@@ -602,9 +608,11 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
onQueueItemDeleted: data.onQueueItemDeleted,
|
||||
onDownloadCleaned: data.onDownloadCleaned,
|
||||
onCategoryChanged: data.onCategoryChanged,
|
||||
mode: data.mode,
|
||||
url: data.url,
|
||||
key: data.key,
|
||||
tags: data.tags,
|
||||
serviceUrls: data.serviceUrls,
|
||||
};
|
||||
|
||||
this.notificationProviderStore.updateProvider({
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { NotificationConfig } from './notification-config.model';
|
||||
import { AppriseMode } from './enums';
|
||||
|
||||
export interface AppriseConfig extends NotificationConfig {
|
||||
mode: AppriseMode;
|
||||
// API mode fields
|
||||
url?: string;
|
||||
key?: string;
|
||||
tags?: string;
|
||||
// CLI mode fields
|
||||
serviceUrls?: string;
|
||||
}
|
||||
|
||||
@@ -15,4 +15,9 @@ export enum NotificationProviderType {
|
||||
Apprise = "Apprise",
|
||||
Ntfy = "Ntfy",
|
||||
Pushover = "Pushover",
|
||||
}
|
||||
|
||||
export enum AppriseMode {
|
||||
Api = "Api",
|
||||
Cli = "Cli",
|
||||
}
|
||||
@@ -25,9 +25,30 @@ Apprise is a universal notification library that supports over 80 different noti
|
||||
Configure Apprise to send notifications through any of its supported services.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
title="Mode"
|
||||
icon="⚙️"
|
||||
>
|
||||
|
||||
Choose how to connect to Apprise:
|
||||
- **API**: Requires an external [Apprise API](https://github.com/caronc/apprise-api) container running. Notifications are sent via HTTP requests to the Apprise server.
|
||||
- **CLI**: Uses the Apprise CLI directly on the host machine. For Docker users, the CLI is pre-installed in the container. Non-Docker users must [install Apprise](https://github.com/caronc/apprise#installation) separately.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<SectionTitle icon="🌐">API Mode</SectionTitle>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Configure settings for API mode. These settings are used when connecting to an external Apprise API server.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
title="URL"
|
||||
icon="🌐"
|
||||
icon="🔗"
|
||||
>
|
||||
|
||||
The Apprise server URL where notification requests will be sent.
|
||||
@@ -54,4 +75,31 @@ Optionally notify only those tagged accordingly. Use a comma (,) to OR your tags
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
<SectionTitle icon="💻">CLI Mode</SectionTitle>
|
||||
|
||||
<p className={styles.sectionDescription}>
|
||||
Configure settings for CLI mode. These settings are used when invoking the Apprise CLI directly.
|
||||
</p>
|
||||
|
||||
<ConfigSection
|
||||
title="Service URLs"
|
||||
icon="📋"
|
||||
>
|
||||
|
||||
Add Apprise service URLs that define where notifications will be sent. Each URL corresponds to a specific notification service.
|
||||
|
||||
Examples:
|
||||
- Discord: `discord://webhook_id/token`
|
||||
- Slack: `slack://token_a/token_b/token_c`
|
||||
- Telegram: `tgram://bot_token/chat_id`
|
||||
- Email: `mailto://user:password@gmail.com`
|
||||
|
||||
For a complete list of supported services and their URL formats, see the [Apprise Wiki](https://github.com/caronc/apprise/wiki#notification-services).
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
Reference in New Issue
Block a user