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.Discord; using Cleanuparr.Infrastructure.Features.Notifications.Models; using Cleanuparr.Infrastructure.Features.Notifications.Telegram; using Cleanuparr.Persistence; using Cleanuparr.Persistence.Models.Configuration.Notification; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Cleanuparr.Api.Features.Notifications.Controllers; [ApiController] [Route("api/configuration/notification_providers")] public sealed class NotificationProvidersController : ControllerBase { private readonly ILogger _logger; private readonly DataContext _dataContext; private readonly INotificationConfigurationService _notificationConfigurationService; private readonly NotificationService _notificationService; private readonly IAppriseCliDetector _appriseCliDetector; public NotificationProvidersController( ILogger logger, DataContext dataContext, INotificationConfigurationService notificationConfigurationService, NotificationService notificationService, IAppriseCliDetector appriseCliDetector) { _logger = logger; _dataContext = dataContext; _notificationConfigurationService = notificationConfigurationService; _notificationService = notificationService; _appriseCliDetector = appriseCliDetector; } [HttpGet] public async Task GetNotificationProviders() { await DataContext.Lock.WaitAsync(); try { var providers = await _dataContext.NotificationConfigs .Include(p => p.NotifiarrConfiguration) .Include(p => p.AppriseConfiguration) .Include(p => p.NtfyConfiguration) .Include(p => p.PushoverConfiguration) .Include(p => p.TelegramConfiguration) .Include(p => p.DiscordConfiguration) .AsNoTracking() .ToListAsync(); var providerDtos = providers .Select(p => new NotificationProviderResponse { Id = p.Id, Name = p.Name, Type = p.Type, IsEnabled = p.IsEnabled, Events = new NotificationEventFlags { OnFailedImportStrike = p.OnFailedImportStrike, OnStalledStrike = p.OnStalledStrike, OnSlowStrike = p.OnSlowStrike, OnQueueItemDeleted = p.OnQueueItemDeleted, OnDownloadCleaned = p.OnDownloadCleaned, OnCategoryChanged = p.OnCategoryChanged }, Configuration = p.Type switch { NotificationProviderType.Notifiarr => p.NotifiarrConfiguration ?? new object(), NotificationProviderType.Apprise => p.AppriseConfiguration ?? new object(), NotificationProviderType.Ntfy => p.NtfyConfiguration ?? new object(), NotificationProviderType.Pushover => p.PushoverConfiguration ?? new object(), NotificationProviderType.Telegram => p.TelegramConfiguration ?? new object(), NotificationProviderType.Discord => p.DiscordConfiguration ?? new object(), _ => new object() } }) .OrderBy(x => x.Type.ToString()) .ThenBy(x => x.Name) .ToList(); var response = new NotificationProvidersResponse { Providers = providerDtos }; return Ok(response); } finally { DataContext.Lock.Release(); } } [HttpGet("apprise/cli-status")] public async Task GetAppriseCliStatus() { string? version = await _appriseCliDetector.GetAppriseVersionAsync(); return Ok(new { Available = version is not null, Version = version }); } [HttpPost("notifiarr")] public async Task CreateNotifiarrProvider([FromBody] CreateNotifiarrProviderRequest newProvider) { await DataContext.Lock.WaitAsync(); try { 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"); } var notifiarrConfig = new NotifiarrConfig { ApiKey = newProvider.ApiKey, ChannelId = newProvider.ChannelId }; notifiarrConfig.Validate(); var provider = new NotificationConfig { Name = newProvider.Name, Type = NotificationProviderType.Notifiarr, IsEnabled = newProvider.IsEnabled, OnFailedImportStrike = newProvider.OnFailedImportStrike, OnStalledStrike = newProvider.OnStalledStrike, OnSlowStrike = newProvider.OnSlowStrike, OnQueueItemDeleted = newProvider.OnQueueItemDeleted, OnDownloadCleaned = newProvider.OnDownloadCleaned, OnCategoryChanged = newProvider.OnCategoryChanged, NotifiarrConfiguration = notifiarrConfig }; _dataContext.NotificationConfigs.Add(provider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(provider); return CreatedAtAction(nameof(GetNotificationProviders), new { id = provider.Id }, providerDto); } catch (Exception ex) { _logger.LogError(ex, "Failed to create Notifiarr provider"); throw; } finally { DataContext.Lock.Release(); } } [HttpPost("apprise")] public async Task CreateAppriseProvider([FromBody] CreateAppriseProviderRequest newProvider) { await DataContext.Lock.WaitAsync(); try { 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"); } var appriseConfig = new AppriseConfig { Mode = newProvider.Mode, Url = newProvider.Url, Key = newProvider.Key, Tags = newProvider.Tags, ServiceUrls = newProvider.ServiceUrls }; appriseConfig.Validate(); var provider = new NotificationConfig { Name = newProvider.Name, Type = NotificationProviderType.Apprise, IsEnabled = newProvider.IsEnabled, OnFailedImportStrike = newProvider.OnFailedImportStrike, OnStalledStrike = newProvider.OnStalledStrike, OnSlowStrike = newProvider.OnSlowStrike, OnQueueItemDeleted = newProvider.OnQueueItemDeleted, OnDownloadCleaned = newProvider.OnDownloadCleaned, OnCategoryChanged = newProvider.OnCategoryChanged, AppriseConfiguration = appriseConfig }; _dataContext.NotificationConfigs.Add(provider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(provider); 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 Apprise provider"); throw; } finally { DataContext.Lock.Release(); } } [HttpPost("ntfy")] public async Task CreateNtfyProvider([FromBody] CreateNtfyProviderRequest newProvider) { await DataContext.Lock.WaitAsync(); try { 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"); } 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 }; ntfyConfig.Validate(); 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 }; _dataContext.NotificationConfigs.Add(provider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(provider); 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(); } } [HttpPost("telegram")] public async Task CreateTelegramProvider([FromBody] CreateTelegramProviderRequest newProvider) { await DataContext.Lock.WaitAsync(); try { 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"); } var telegramConfig = new TelegramConfig { BotToken = newProvider.BotToken, ChatId = newProvider.ChatId, TopicId = newProvider.TopicId, SendSilently = newProvider.SendSilently }; telegramConfig.Validate(); var provider = new NotificationConfig { Name = newProvider.Name, Type = NotificationProviderType.Telegram, IsEnabled = newProvider.IsEnabled, OnFailedImportStrike = newProvider.OnFailedImportStrike, OnStalledStrike = newProvider.OnStalledStrike, OnSlowStrike = newProvider.OnSlowStrike, OnQueueItemDeleted = newProvider.OnQueueItemDeleted, OnDownloadCleaned = newProvider.OnDownloadCleaned, OnCategoryChanged = newProvider.OnCategoryChanged, TelegramConfiguration = telegramConfig }; _dataContext.NotificationConfigs.Add(provider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(provider); 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 Telegram provider"); throw; } finally { DataContext.Lock.Release(); } } [HttpPut("notifiarr/{id:guid}")] public async Task UpdateNotifiarrProvider(Guid id, [FromBody] UpdateNotifiarrProviderRequest updatedProvider) { await DataContext.Lock.WaitAsync(); try { var existingProvider = await _dataContext.NotificationConfigs .Include(p => p.NotifiarrConfiguration) .FirstOrDefaultAsync(p => p.Id == id && p.Type == NotificationProviderType.Notifiarr); if (existingProvider == null) { return NotFound($"Notifiarr provider with ID {id} not found"); } 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"); } var notifiarrConfig = new NotifiarrConfig { ApiKey = updatedProvider.ApiKey, ChannelId = updatedProvider.ChannelId }; if (existingProvider.NotifiarrConfiguration != null) { notifiarrConfig = notifiarrConfig with { Id = existingProvider.NotifiarrConfiguration.Id }; } notifiarrConfig.Validate(); 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, NotifiarrConfiguration = notifiarrConfig, UpdatedAt = DateTime.UtcNow }; _dataContext.NotificationConfigs.Remove(existingProvider); _dataContext.NotificationConfigs.Add(newProvider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(newProvider); return Ok(providerDto); } catch (ValidationException ex) { return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Failed to update Notifiarr provider with ID {Id}", id); throw; } finally { DataContext.Lock.Release(); } } [HttpPut("apprise/{id:guid}")] public async Task UpdateAppriseProvider(Guid id, [FromBody] UpdateAppriseProviderRequest updatedProvider) { await DataContext.Lock.WaitAsync(); try { var existingProvider = await _dataContext.NotificationConfigs .Include(p => p.AppriseConfiguration) .FirstOrDefaultAsync(p => p.Id == id && p.Type == NotificationProviderType.Apprise); if (existingProvider == null) { return NotFound($"Apprise provider with ID {id} not found"); } 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"); } var appriseConfig = new AppriseConfig { Mode = updatedProvider.Mode, Url = updatedProvider.Url, Key = updatedProvider.Key, Tags = updatedProvider.Tags, ServiceUrls = updatedProvider.ServiceUrls }; if (existingProvider.AppriseConfiguration != null) { appriseConfig = appriseConfig with { Id = existingProvider.AppriseConfiguration.Id }; } appriseConfig.Validate(); 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, AppriseConfiguration = appriseConfig, UpdatedAt = DateTime.UtcNow }; _dataContext.NotificationConfigs.Remove(existingProvider); _dataContext.NotificationConfigs.Add(newProvider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(newProvider); return Ok(providerDto); } catch (ValidationException ex) { return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Failed to update Apprise provider with ID {Id}", id); throw; } finally { DataContext.Lock.Release(); } } [HttpPut("ntfy/{id:guid}")] public async Task UpdateNtfyProvider(Guid id, [FromBody] UpdateNtfyProviderRequest updatedProvider) { await DataContext.Lock.WaitAsync(); try { 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"); } 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"); } 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 }; if (existingProvider.NtfyConfiguration != null) { ntfyConfig = ntfyConfig with { Id = existingProvider.NtfyConfiguration.Id }; } ntfyConfig.Validate(); 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 }; _dataContext.NotificationConfigs.Remove(existingProvider); _dataContext.NotificationConfigs.Add(newProvider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(newProvider); 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(); } } [HttpPut("telegram/{id:guid}")] public async Task UpdateTelegramProvider(Guid id, [FromBody] UpdateTelegramProviderRequest updatedProvider) { await DataContext.Lock.WaitAsync(); try { var existingProvider = await _dataContext.NotificationConfigs .Include(p => p.TelegramConfiguration) .FirstOrDefaultAsync(p => p.Id == id && p.Type == NotificationProviderType.Telegram); if (existingProvider == null) { return NotFound($"Telegram provider with ID {id} not found"); } 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"); } var telegramConfig = new TelegramConfig { BotToken = updatedProvider.BotToken, ChatId = updatedProvider.ChatId, TopicId = updatedProvider.TopicId, SendSilently = updatedProvider.SendSilently }; if (existingProvider.TelegramConfiguration != null) { telegramConfig = telegramConfig with { Id = existingProvider.TelegramConfiguration.Id }; } telegramConfig.Validate(); 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, TelegramConfiguration = telegramConfig, UpdatedAt = DateTime.UtcNow }; _dataContext.NotificationConfigs.Remove(existingProvider); _dataContext.NotificationConfigs.Add(newProvider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(newProvider); return Ok(providerDto); } catch (ValidationException ex) { return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Failed to update Telegram provider with ID {Id}", id); throw; } finally { DataContext.Lock.Release(); } } [HttpDelete("{id:guid}")] public async Task DeleteNotificationProvider(Guid id) { await DataContext.Lock.WaitAsync(); try { var existingProvider = await _dataContext.NotificationConfigs .Include(p => p.NotifiarrConfiguration) .Include(p => p.AppriseConfiguration) .Include(p => p.NtfyConfiguration) .Include(p => p.PushoverConfiguration) .Include(p => p.TelegramConfiguration) .Include(p => p.DiscordConfiguration) .FirstOrDefaultAsync(p => p.Id == id); if (existingProvider == null) { return NotFound($"Notification provider with ID {id} not found"); } _dataContext.NotificationConfigs.Remove(existingProvider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); _logger.LogInformation("Removed notification provider {ProviderName} with ID {ProviderId}", existingProvider.Name, existingProvider.Id); return NoContent(); } catch (Exception ex) { _logger.LogError(ex, "Failed to delete notification provider with ID {Id}", id); throw; } finally { DataContext.Lock.Release(); } } [HttpPost("notifiarr/test")] public async Task TestNotifiarrProvider([FromBody] TestNotifiarrProviderRequest testRequest) { try { var notifiarrConfig = new NotifiarrConfig { ApiKey = testRequest.ApiKey, ChannelId = testRequest.ChannelId }; notifiarrConfig.Validate(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Notifiarr, IsEnabled = true, Events = new NotificationEventFlags { OnFailedImportStrike = true, OnStalledStrike = false, OnSlowStrike = false, OnQueueItemDeleted = false, OnDownloadCleaned = false, OnCategoryChanged = false }, Configuration = notifiarrConfig }; await _notificationService.SendTestNotificationAsync(providerDto); return Ok(new { Message = "Test notification sent successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Failed to test Notifiarr provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } } [HttpPost("apprise/test")] public async Task TestAppriseProvider([FromBody] TestAppriseProviderRequest testRequest) { try { var appriseConfig = new AppriseConfig { Mode = testRequest.Mode, Url = testRequest.Url, Key = testRequest.Key, Tags = testRequest.Tags, ServiceUrls = testRequest.ServiceUrls }; appriseConfig.Validate(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Apprise, IsEnabled = true, Events = new NotificationEventFlags { OnFailedImportStrike = true, OnStalledStrike = false, OnSlowStrike = false, OnQueueItemDeleted = false, OnDownloadCleaned = false, OnCategoryChanged = false }, Configuration = appriseConfig }; await _notificationService.SendTestNotificationAsync(providerDto); return Ok(new { Message = "Test notification sent successfully" }); } catch (AppriseException exception) { return StatusCode((int)HttpStatusCode.InternalServerError, exception.Message); } catch (Exception ex) { _logger.LogError(ex, "Failed to test Apprise provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } } [HttpPost("ntfy/test")] public async Task TestNtfyProvider([FromBody] TestNtfyProviderRequest testRequest) { try { 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 }; ntfyConfig.Validate(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Ntfy, IsEnabled = true, Events = new NotificationEventFlags { OnFailedImportStrike = true, OnStalledStrike = false, OnSlowStrike = false, OnQueueItemDeleted = false, OnDownloadCleaned = false, OnCategoryChanged = false }, Configuration = ntfyConfig }; await _notificationService.SendTestNotificationAsync(providerDto); return Ok(new { Message = "Test notification sent successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Failed to test Ntfy provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } } [HttpPost("telegram/test")] public async Task TestTelegramProvider([FromBody] TestTelegramProviderRequest testRequest) { try { var telegramConfig = new TelegramConfig { BotToken = testRequest.BotToken, ChatId = testRequest.ChatId, TopicId = testRequest.TopicId, SendSilently = testRequest.SendSilently }; telegramConfig.Validate(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Telegram, IsEnabled = true, Events = new NotificationEventFlags { OnFailedImportStrike = true, OnStalledStrike = false, OnSlowStrike = false, OnQueueItemDeleted = false, OnDownloadCleaned = false, OnCategoryChanged = false }, Configuration = telegramConfig }; await _notificationService.SendTestNotificationAsync(providerDto); return Ok(new { Message = "Test notification sent successfully" }); } catch (TelegramException ex) { _logger.LogWarning(ex, "Failed to test Telegram provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } catch (Exception ex) { _logger.LogError(ex, "Failed to test Telegram provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } } private static NotificationProviderResponse MapProvider(NotificationConfig provider) { return new NotificationProviderResponse { 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.Type switch { NotificationProviderType.Notifiarr => provider.NotifiarrConfiguration ?? new object(), NotificationProviderType.Apprise => provider.AppriseConfiguration ?? new object(), NotificationProviderType.Ntfy => provider.NtfyConfiguration ?? new object(), NotificationProviderType.Pushover => provider.PushoverConfiguration ?? new object(), NotificationProviderType.Telegram => provider.TelegramConfiguration ?? new object(), NotificationProviderType.Discord => provider.DiscordConfiguration ?? new object(), _ => new object() } }; } [HttpPost("discord")] public async Task CreateDiscordProvider([FromBody] CreateDiscordProviderRequest newProvider) { await DataContext.Lock.WaitAsync(); try { 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"); } var discordConfig = new DiscordConfig { WebhookUrl = newProvider.WebhookUrl, Username = newProvider.Username, AvatarUrl = newProvider.AvatarUrl }; discordConfig.Validate(); var provider = new NotificationConfig { Name = newProvider.Name, Type = NotificationProviderType.Discord, IsEnabled = newProvider.IsEnabled, OnFailedImportStrike = newProvider.OnFailedImportStrike, OnStalledStrike = newProvider.OnStalledStrike, OnSlowStrike = newProvider.OnSlowStrike, OnQueueItemDeleted = newProvider.OnQueueItemDeleted, OnDownloadCleaned = newProvider.OnDownloadCleaned, OnCategoryChanged = newProvider.OnCategoryChanged, DiscordConfiguration = discordConfig }; _dataContext.NotificationConfigs.Add(provider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(provider); 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 Discord provider"); throw; } finally { DataContext.Lock.Release(); } } [HttpPut("discord/{id:guid}")] public async Task UpdateDiscordProvider(Guid id, [FromBody] UpdateDiscordProviderRequest updatedProvider) { await DataContext.Lock.WaitAsync(); try { var existingProvider = await _dataContext.NotificationConfigs .Include(p => p.DiscordConfiguration) .FirstOrDefaultAsync(p => p.Id == id && p.Type == NotificationProviderType.Discord); if (existingProvider == null) { return NotFound($"Discord provider with ID {id} not found"); } 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"); } var discordConfig = new DiscordConfig { WebhookUrl = updatedProvider.WebhookUrl, Username = updatedProvider.Username, AvatarUrl = updatedProvider.AvatarUrl }; if (existingProvider.DiscordConfiguration != null) { discordConfig = discordConfig with { Id = existingProvider.DiscordConfiguration.Id }; } discordConfig.Validate(); 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, DiscordConfiguration = discordConfig, UpdatedAt = DateTime.UtcNow }; _dataContext.NotificationConfigs.Remove(existingProvider); _dataContext.NotificationConfigs.Add(newProvider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(newProvider); return Ok(providerDto); } catch (ValidationException ex) { return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Failed to update Discord provider with ID {Id}", id); throw; } finally { DataContext.Lock.Release(); } } [HttpPost("discord/test")] public async Task TestDiscordProvider([FromBody] TestDiscordProviderRequest testRequest) { try { var discordConfig = new DiscordConfig { WebhookUrl = testRequest.WebhookUrl, Username = testRequest.Username, AvatarUrl = testRequest.AvatarUrl }; discordConfig.Validate(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Discord, IsEnabled = true, Events = new NotificationEventFlags { OnFailedImportStrike = true, OnStalledStrike = false, OnSlowStrike = false, OnQueueItemDeleted = false, OnDownloadCleaned = false, OnCategoryChanged = false }, Configuration = discordConfig }; await _notificationService.SendTestNotificationAsync(providerDto); return Ok(new { Message = "Test notification sent successfully" }); } catch (DiscordException ex) { _logger.LogWarning(ex, "Failed to test Discord provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } catch (Exception ex) { _logger.LogError(ex, "Failed to test Discord provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } } [HttpPost("pushover")] public async Task CreatePushoverProvider([FromBody] CreatePushoverProviderRequest newProvider) { await DataContext.Lock.WaitAsync(); try { 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"); } var pushoverConfig = new PushoverConfig { ApiToken = newProvider.ApiToken, UserKey = newProvider.UserKey, Devices = newProvider.Devices, Priority = newProvider.Priority, Sound = newProvider.Sound, Retry = newProvider.Retry, Expire = newProvider.Expire, Tags = newProvider.Tags }; pushoverConfig.Validate(); var provider = new NotificationConfig { Name = newProvider.Name, Type = NotificationProviderType.Pushover, IsEnabled = newProvider.IsEnabled, OnFailedImportStrike = newProvider.OnFailedImportStrike, OnStalledStrike = newProvider.OnStalledStrike, OnSlowStrike = newProvider.OnSlowStrike, OnQueueItemDeleted = newProvider.OnQueueItemDeleted, OnDownloadCleaned = newProvider.OnDownloadCleaned, OnCategoryChanged = newProvider.OnCategoryChanged, PushoverConfiguration = pushoverConfig }; _dataContext.NotificationConfigs.Add(provider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(provider); 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 Pushover provider"); throw; } finally { DataContext.Lock.Release(); } } [HttpPut("pushover/{id:guid}")] public async Task UpdatePushoverProvider(Guid id, [FromBody] UpdatePushoverProviderRequest updatedProvider) { await DataContext.Lock.WaitAsync(); try { var existingProvider = await _dataContext.NotificationConfigs .Include(p => p.PushoverConfiguration) .FirstOrDefaultAsync(p => p.Id == id && p.Type == NotificationProviderType.Pushover); if (existingProvider == null) { return NotFound($"Pushover provider with ID {id} not found"); } 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"); } var pushoverConfig = new PushoverConfig { ApiToken = updatedProvider.ApiToken, UserKey = updatedProvider.UserKey, Devices = updatedProvider.Devices, Priority = updatedProvider.Priority, Sound = updatedProvider.Sound, Retry = updatedProvider.Retry, Expire = updatedProvider.Expire, Tags = updatedProvider.Tags }; if (existingProvider.PushoverConfiguration != null) { pushoverConfig = pushoverConfig with { Id = existingProvider.PushoverConfiguration.Id }; } pushoverConfig.Validate(); 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, PushoverConfiguration = pushoverConfig, UpdatedAt = DateTime.UtcNow }; _dataContext.NotificationConfigs.Remove(existingProvider); _dataContext.NotificationConfigs.Add(newProvider); await _dataContext.SaveChangesAsync(); await _notificationConfigurationService.InvalidateCacheAsync(); var providerDto = MapProvider(newProvider); return Ok(providerDto); } catch (ValidationException ex) { return BadRequest(ex.Message); } catch (Exception ex) { _logger.LogError(ex, "Failed to update Pushover provider with ID {Id}", id); throw; } finally { DataContext.Lock.Release(); } } [HttpPost("pushover/test")] public async Task TestPushoverProvider([FromBody] TestPushoverProviderRequest testRequest) { try { var pushoverConfig = new PushoverConfig { ApiToken = testRequest.ApiToken, UserKey = testRequest.UserKey, Devices = testRequest.Devices, Priority = testRequest.Priority, Sound = testRequest.Sound, Retry = testRequest.Retry, Expire = testRequest.Expire, Tags = testRequest.Tags }; pushoverConfig.Validate(); var providerDto = new NotificationProviderDto { Id = Guid.NewGuid(), Name = "Test Provider", Type = NotificationProviderType.Pushover, IsEnabled = true, Events = new NotificationEventFlags { OnFailedImportStrike = true, OnStalledStrike = false, OnSlowStrike = false, OnQueueItemDeleted = false, OnDownloadCleaned = false, OnCategoryChanged = false }, Configuration = pushoverConfig }; await _notificationService.SendTestNotificationAsync(providerDto); return Ok(new { Message = "Test notification sent successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Failed to test Pushover provider"); return BadRequest(new { Message = $"Test failed: {ex.Message}" }); } } }