Compare commits

..

1 Commits

Author SHA1 Message Date
Flaminel
1d1e8679e4 Add supported apps version disclaimer (#399) 2025-12-30 00:26:16 +02:00
39 changed files with 17 additions and 2431 deletions

View File

@@ -34,14 +34,14 @@ https://cleanuparr.github.io/Cleanuparr/docs/screenshots
## 🎯 Supported Applications
### *Arr Applications
### *Arr Applications (latest version)
- **Sonarr**
- **Radarr**
- **Lidarr**
- **Readarr**
- **Whisparr**
- **Whisparr v2**
### Download Clients
### Download Clients (latest version)
- **qBittorrent**
- **Transmission**
- **Deluge**

View File

@@ -3,7 +3,6 @@ using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
using Cleanuparr.Infrastructure.Features.Notifications.Notifiarr;
using Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
using Cleanuparr.Infrastructure.Features.Notifications.Pushover;
using Cleanuparr.Infrastructure.Features.Notifications.Telegram;
namespace Cleanuparr.Api.DependencyInjection;
@@ -17,7 +16,6 @@ public static class NotificationsDI
.AddSingleton<IAppriseCliDetector, AppriseCliDetector>()
.AddScoped<INtfyProxy, NtfyProxy>()
.AddScoped<IPushoverProxy, PushoverProxy>()
.AddScoped<ITelegramProxy, TelegramProxy>()
.AddScoped<INotificationConfigurationService, NotificationConfigurationService>()
.AddScoped<INotificationProviderFactory, NotificationProviderFactory>()
.AddScoped<NotificationProviderFactory>()

View File

@@ -1,12 +0,0 @@
namespace Cleanuparr.Api.Features.Notifications.Contracts.Requests;
public sealed record CreateTelegramProviderRequest : CreateNotificationProviderRequestBase
{
public string BotToken { get; init; } = string.Empty;
public string ChatId { get; init; } = string.Empty;
public string? TopicId { get; init; }
public bool SendSilently { get; init; }
}

View File

@@ -1,12 +0,0 @@
namespace Cleanuparr.Api.Features.Notifications.Contracts.Requests;
public sealed record TestTelegramProviderRequest
{
public string BotToken { get; init; } = string.Empty;
public string ChatId { get; init; } = string.Empty;
public string? TopicId { get; init; }
public bool SendSilently { get; init; }
}

View File

@@ -1,12 +0,0 @@
namespace Cleanuparr.Api.Features.Notifications.Contracts.Requests;
public sealed record UpdateTelegramProviderRequest : CreateNotificationProviderRequestBase
{
public string BotToken { get; init; } = string.Empty;
public string ChatId { get; init; } = string.Empty;
public string? TopicId { get; init; }
public bool SendSilently { get; init; }
}

View File

@@ -6,7 +6,6 @@ using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Features.Notifications;
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
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;
@@ -49,7 +48,6 @@ public sealed class NotificationProvidersController : ControllerBase
.Include(p => p.AppriseConfiguration)
.Include(p => p.NtfyConfiguration)
.Include(p => p.PushoverConfiguration)
.Include(p => p.TelegramConfiguration)
.AsNoTracking()
.ToListAsync();
@@ -75,7 +73,6 @@ public sealed class NotificationProvidersController : ControllerBase
NotificationProviderType.Apprise => p.AppriseConfiguration ?? new object(),
NotificationProviderType.Ntfy => p.NtfyConfiguration ?? new object(),
NotificationProviderType.Pushover => p.PushoverConfiguration ?? new object(),
NotificationProviderType.Telegram => p.TelegramConfiguration ?? new object(),
_ => new object()
}
})
@@ -292,69 +289,6 @@ public sealed class NotificationProvidersController : ControllerBase
}
}
[HttpPost("telegram")]
public async Task<IActionResult> 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<IActionResult> UpdateNotifiarrProvider(Guid id, [FromBody] UpdateNotifiarrProviderRequest updatedProvider)
{
@@ -601,87 +535,6 @@ public sealed class NotificationProvidersController : ControllerBase
}
}
[HttpPut("telegram/{id:guid}")]
public async Task<IActionResult> 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<IActionResult> DeleteNotificationProvider(Guid id)
{
@@ -693,7 +546,6 @@ public sealed class NotificationProvidersController : ControllerBase
.Include(p => p.AppriseConfiguration)
.Include(p => p.NtfyConfiguration)
.Include(p => p.PushoverConfiguration)
.Include(p => p.TelegramConfiguration)
.FirstOrDefaultAsync(p => p.Id == id);
if (existingProvider == null)
@@ -855,53 +707,6 @@ public sealed class NotificationProvidersController : ControllerBase
}
}
[HttpPost("telegram/test")]
public async Task<IActionResult> 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
@@ -925,7 +730,6 @@ public sealed class NotificationProvidersController : ControllerBase
NotificationProviderType.Apprise => provider.AppriseConfiguration ?? new object(),
NotificationProviderType.Ntfy => provider.NtfyConfiguration ?? new object(),
NotificationProviderType.Pushover => provider.PushoverConfiguration ?? new object(),
NotificationProviderType.Telegram => provider.TelegramConfiguration ?? new object(),
_ => new object()
}
};

View File

@@ -5,6 +5,5 @@ public enum NotificationProviderType
Notifiarr,
Apprise,
Ntfy,
Pushover,
Telegram,
Pushover
}

View File

@@ -87,7 +87,6 @@ public sealed class NotificationConfigurationService : INotificationConfiguratio
.Include(p => p.AppriseConfiguration)
.Include(p => p.NtfyConfiguration)
.Include(p => p.PushoverConfiguration)
.Include(p => p.TelegramConfiguration)
.AsNoTracking()
.ToListAsync();
@@ -138,7 +137,6 @@ public sealed class NotificationConfigurationService : INotificationConfiguratio
NotificationProviderType.Apprise => config.AppriseConfiguration,
NotificationProviderType.Ntfy => config.NtfyConfiguration,
NotificationProviderType.Pushover => config.PushoverConfiguration,
NotificationProviderType.Telegram => config.TelegramConfiguration,
_ => throw new ArgumentOutOfRangeException(nameof(config), $"Config type for provider type {config.Type.ToString()} is not registered")
};

View File

@@ -1,11 +1,9 @@
using Cleanuparr.Domain.Entities;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Features.Notifications.Apprise;
using Cleanuparr.Infrastructure.Features.Notifications.Models;
using Cleanuparr.Infrastructure.Features.Notifications.Notifiarr;
using Cleanuparr.Infrastructure.Features.Notifications.Ntfy;
using Cleanuparr.Infrastructure.Features.Notifications.Pushover;
using Cleanuparr.Infrastructure.Features.Notifications.Telegram;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Microsoft.Extensions.DependencyInjection;
@@ -28,7 +26,6 @@ public sealed class NotificationProviderFactory : INotificationProviderFactory
NotificationProviderType.Apprise => CreateAppriseProvider(config),
NotificationProviderType.Ntfy => CreateNtfyProvider(config),
NotificationProviderType.Pushover => CreatePushoverProvider(config),
NotificationProviderType.Telegram => CreateTelegramProvider(config),
_ => throw new NotSupportedException($"Provider type {config.Type} is not supported")
};
}
@@ -65,12 +62,4 @@ public sealed class NotificationProviderFactory : INotificationProviderFactory
return new PushoverProvider(config.Name, config.Type, pushoverConfig, proxy);
}
private INotificationProvider CreateTelegramProvider(NotificationProviderDto config)
{
var telegramConfig = (TelegramConfig)config.Configuration;
var proxy = _serviceProvider.GetRequiredService<ITelegramProxy>();
return new TelegramProvider(config.Name, config.Type, telegramConfig, proxy);
}
}

View File

@@ -1,6 +0,0 @@
namespace Cleanuparr.Infrastructure.Features.Notifications.Telegram;
public interface ITelegramProxy
{
Task SendNotification(TelegramPayload payload, string botToken);
}

View File

@@ -1,12 +0,0 @@
namespace Cleanuparr.Infrastructure.Features.Notifications.Telegram;
public sealed class TelegramException : Exception
{
public TelegramException(string message) : base(message)
{
}
public TelegramException(string message, Exception innerException) : base(message, innerException)
{
}
}

View File

@@ -1,21 +0,0 @@
using Newtonsoft.Json;
namespace Cleanuparr.Infrastructure.Features.Notifications.Telegram;
public sealed class TelegramPayload
{
[JsonProperty("chat_id")]
public string ChatId { get; init; } = string.Empty;
[JsonProperty("text")]
public string Text { get; init; } = string.Empty;
[JsonProperty("photo")]
public string? PhotoUrl { get; init; }
[JsonProperty("message_thread_id")]
public int? MessageThreadId { get; init; }
[JsonProperty("disable_notification")]
public bool DisableNotification { get; init; }
}

View File

@@ -1,74 +0,0 @@
using System.Net;
using System.Text;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Features.Notifications.Models;
using Cleanuparr.Persistence.Models.Configuration.Notification;
namespace Cleanuparr.Infrastructure.Features.Notifications.Telegram;
public sealed class TelegramProvider : NotificationProviderBase<TelegramConfig>
{
private readonly ITelegramProxy _proxy;
public TelegramProvider(
string name,
NotificationProviderType type,
TelegramConfig config,
ITelegramProxy proxy
) : base(name, type, config)
{
_proxy = proxy;
}
public override async Task SendNotificationAsync(NotificationContext context)
{
var payload = BuildPayload(context);
await _proxy.SendNotification(payload, Config.BotToken);
}
private TelegramPayload BuildPayload(NotificationContext context)
{
return new TelegramPayload
{
ChatId = Config.ChatId.Trim(),
MessageThreadId = ParseTopicId(Config.TopicId),
DisableNotification = Config.SendSilently,
Text = BuildMessage(context),
PhotoUrl = context.Image?.ToString()
};
}
private static string BuildMessage(NotificationContext context)
{
var builder = new StringBuilder();
if (!string.IsNullOrWhiteSpace(context.Title))
{
builder.AppendLine(HtmlEncode(context.Title.Trim()));
builder.AppendLine();
}
if (!string.IsNullOrWhiteSpace(context.Description))
{
builder.AppendLine(HtmlEncode(context.Description.Trim()));
}
if (context.Data.Any())
{
builder.AppendLine();
foreach ((string key, string value) in context.Data)
{
builder.AppendLine($"{HtmlEncode(key)}: {HtmlEncode(value)}");
}
}
return builder.ToString().Trim();
}
private static string HtmlEncode(string value) => WebUtility.HtmlEncode(value);
private static int? ParseTopicId(string? topicId)
{
return int.TryParse(topicId, out int parsed) ? parsed : null;
}
}

View File

@@ -1,109 +0,0 @@
using System.Text;
using Cleanuparr.Shared.Helpers;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System.Net;
namespace Cleanuparr.Infrastructure.Features.Notifications.Telegram;
public sealed class TelegramProxy : ITelegramProxy
{
private readonly HttpClient _httpClient;
public TelegramProxy(IHttpClientFactory httpClientFactory)
{
_httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
}
public async Task SendNotification(TelegramPayload payload, string botToken)
{
bool hasImage = !string.IsNullOrWhiteSpace(payload.PhotoUrl);
bool captionFits = payload.Text.Length <= 1024;
bool usePhoto = hasImage && captionFits;
string endpoint = usePhoto ? "sendPhoto" : "sendMessage";
string url = $"https://api.telegram.org/bot{botToken}/{endpoint}";
string text = payload.Text;
if (hasImage && !usePhoto)
{
text = $"{payload.Text}\n{BuildInvisibleImageLink(payload.PhotoUrl!)}";
}
object body = usePhoto
? new
{
chat_id = payload.ChatId,
message_thread_id = payload.MessageThreadId,
disable_notification = payload.DisableNotification,
photo = payload.PhotoUrl,
caption = text,
parse_mode = "HTML"
}
: new
{
chat_id = payload.ChatId,
message_thread_id = payload.MessageThreadId,
disable_notification = payload.DisableNotification,
text,
parse_mode = "HTML",
disable_web_page_preview = !hasImage ? true : false
};
try
{
string content = JsonConvert.SerializeObject(body, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore
});
using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(content, Encoding.UTF8, "application/json");
using HttpResponseMessage response = await _httpClient.SendAsync(request);
if (response.IsSuccessStatusCode)
{
return;
}
string bodyContent = await response.Content.ReadAsStringAsync();
throw MapToException(response.StatusCode, bodyContent);
}
catch (HttpRequestException ex)
{
throw new TelegramException("Unable to reach Telegram API", ex);
}
}
private static TelegramException MapToException(HttpStatusCode statusCode, string responseBody)
{
return statusCode switch
{
HttpStatusCode.BadRequest => new TelegramException($"Telegram rejected the request: {Truncate(responseBody)}"),
HttpStatusCode.Unauthorized => new TelegramException("Telegram bot token is invalid"),
HttpStatusCode.Forbidden => new TelegramException("Bot does not have permission to message the chat"),
HttpStatusCode.TooManyRequests => new TelegramException("Rate limited by Telegram"),
_ => new TelegramException($"Telegram API error ({(int)statusCode}): {Truncate(responseBody)}")
};
}
private static string BuildInvisibleImageLink(string imageUrl)
{
// Zero-width space to force a preview without visible text as described in https://stackoverflow.com/a/55126912
return $"<a href=\"{WebUtility.HtmlEncode(imageUrl)}\">&#8203;</a>";
}
private static string Truncate(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
const int limit = 500;
return value.Length <= limit ? value : value[..limit];
}
}

View File

@@ -53,8 +53,6 @@ public class DataContext : DbContext
public DbSet<NtfyConfig> NtfyConfigs { get; set; }
public DbSet<PushoverConfig> PushoverConfigs { get; set; }
public DbSet<TelegramConfig> TelegramConfigs { get; set; }
public DbSet<BlacklistSyncHistory> BlacklistSyncHistory { get; set; }
@@ -151,11 +149,6 @@ public class DataContext : DbContext
.HasForeignKey<PushoverConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasOne(p => p.TelegramConfiguration)
.WithOne(c => c.NotificationConfig)
.HasForeignKey<TelegramConfig>(c => c.NotificationConfigId)
.OnDelete(DeleteBehavior.Cascade);
entity.HasIndex(p => p.Name).IsUnique();
});

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,50 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Cleanuparr.Persistence.Migrations.Data
{
/// <inheritdoc />
public partial class AddTelegram : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "telegram_configs",
columns: table => new
{
id = table.Column<Guid>(type: "TEXT", nullable: false),
notification_config_id = table.Column<Guid>(type: "TEXT", nullable: false),
bot_token = table.Column<string>(type: "TEXT", maxLength: 255, nullable: false),
chat_id = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
topic_id = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
send_silently = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_telegram_configs", x => x.id);
table.ForeignKey(
name: "fk_telegram_configs_notification_configs_notification_config_id",
column: x => x.notification_config_id,
principalTable: "notification_configs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "ix_telegram_configs_notification_config_id",
table: "telegram_configs",
column: "notification_config_id",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "telegram_configs");
}
}
}

View File

@@ -738,48 +738,6 @@ namespace Cleanuparr.Persistence.Migrations.Data
b.ToTable("pushover_configs", (string)null);
});
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.TelegramConfig", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasColumnName("id");
b.Property<string>("BotToken")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.HasColumnName("bot_token");
b.Property<string>("ChatId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("chat_id");
b.Property<Guid>("NotificationConfigId")
.HasColumnType("TEXT")
.HasColumnName("notification_config_id");
b.Property<bool>("SendSilently")
.HasColumnType("INTEGER")
.HasColumnName("send_silently");
b.Property<string>("TopicId")
.HasMaxLength(100)
.HasColumnType("TEXT")
.HasColumnName("topic_id");
b.HasKey("Id")
.HasName("pk_telegram_configs");
b.HasIndex("NotificationConfigId")
.IsUnique()
.HasDatabaseName("ix_telegram_configs_notification_config_id");
b.ToTable("telegram_configs", (string)null);
});
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
{
b.Property<Guid>("Id")
@@ -1075,18 +1033,6 @@ namespace Cleanuparr.Persistence.Migrations.Data
b.Navigation("NotificationConfig");
});
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.TelegramConfig", b =>
{
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig")
.WithOne("TelegramConfiguration")
.HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.TelegramConfig", "NotificationConfigId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("fk_telegram_configs_notification_configs_notification_config_id");
b.Navigation("NotificationConfig");
});
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.SlowRule", b =>
{
b.HasOne("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", "QueueCleanerConfig")
@@ -1142,8 +1088,6 @@ namespace Cleanuparr.Persistence.Migrations.Data
b.Navigation("NtfyConfiguration");
b.Navigation("PushoverConfiguration");
b.Navigation("TelegramConfiguration");
});
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>

View File

@@ -43,8 +43,6 @@ public sealed record NotificationConfig
public PushoverConfig? PushoverConfiguration { get; init; }
public TelegramConfig? TelegramConfiguration { get; init; }
[NotMapped]
public bool IsConfigured => Type switch
{
@@ -52,7 +50,6 @@ public sealed record NotificationConfig
NotificationProviderType.Apprise => AppriseConfiguration?.IsValid() == true,
NotificationProviderType.Ntfy => NtfyConfiguration?.IsValid() == true,
NotificationProviderType.Pushover => PushoverConfiguration?.IsValid() == true,
NotificationProviderType.Telegram => TelegramConfiguration?.IsValid() == true,
_ => throw new ArgumentOutOfRangeException(nameof(Type), $"Invalid notification provider type {Type}")
};

View File

@@ -1,82 +0,0 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Persistence.Models.Configuration;
using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.Notification;
public sealed record TelegramConfig : IConfig
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; init; } = Guid.NewGuid();
[Required]
public Guid NotificationConfigId { get; init; }
public NotificationConfig NotificationConfig { get; init; } = null!;
[Required]
[MaxLength(255)]
public string BotToken { get; init; } = string.Empty;
[Required]
[MaxLength(100)]
public string ChatId { get; init; } = string.Empty;
[MaxLength(100)]
public string? TopicId { get; init; }
public bool SendSilently { get; init; }
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(BotToken)
&& !string.IsNullOrWhiteSpace(ChatId)
&& IsChatIdValid(ChatId)
&& IsTopicValid(TopicId);
}
public void Validate()
{
if (string.IsNullOrWhiteSpace(BotToken))
{
throw new ValidationException("Telegram bot token is required");
}
if (BotToken.Length < 10)
{
throw new ValidationException("Telegram bot token must be at least 10 characters long");
}
if (string.IsNullOrWhiteSpace(ChatId))
{
throw new ValidationException("Telegram chat ID is required");
}
if (!IsChatIdValid(ChatId))
{
throw new ValidationException("Telegram chat ID must be a valid integer (negative IDs allowed for groups)");
}
if (!IsTopicValid(TopicId))
{
throw new ValidationException("Telegram topic ID must be a valid integer when specified");
}
}
private static bool IsChatIdValid(string chatId)
{
return long.TryParse(chatId, out _);
}
private static bool IsTopicValid(string? topicId)
{
if (string.IsNullOrWhiteSpace(topicId))
{
return true;
}
return int.TryParse(topicId, out _);
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M256 0C114.6 0 0 114.6 0 256s114.6 256 256 256 256-114.6 256-256S397.4 0 256 0m-46.1 291.2c-.7.8-2.8 3.4-2.5 6.7l-4 42.5-21.3-59c2.1-1.3 5-3.2 8.7-5.5 51-32.1 88.1-55.3 111.1-69.4-22.2 21.6-58 54.4-91.6 84.3zm4 66.3 4.5-48.3c6.6 4.4 16 10.8 26.5 18-17.4 17.7-26.4 26.2-31 30.3m163-202.7v.3c0 .9-.1 1.9-.2 3.2-.1.5-.1 1.1-.2 1.7v.1c-1.5 23-45.1 198.8-45.5 200.6-.1.3-1.8 6.5-7 6.7-3.2.1-6.3-1.1-8.5-3.3l-.3-.3c-17.6-15.1-74.7-53.8-94.4-66.9 7.8-7 30.7-27.5 53.5-48.6 57.8-53.3 59.3-58.5 60.1-61.3l.1-.2c.5-2.2-.1-4.4-1.6-5.9-1.7-1.7-4.2-2.3-6.9-1.6l-.5.2c-4.9 1.8-47 27.7-140.7 86.8-4.3 2.7-7.6 4.8-9.7 6.1l-61.7-20.1c-1.7-.8-2.3-1.7-1.9-2.9 0-.1.4-.6 2.5-2 9.8-6.7 157.8-61.1 255-96 2.4-.8 5.9-1.3 7.2-.8l.3.1c.1 0 .2.1.2.2v.2c.1 1.1.3 2.5.2 3.7" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 864 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><linearGradient id="telegram_svg__a" x1="256" x2="256" y1="790" y2="278" gradientTransform="translate(0 -278)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#1d93d2"/><stop offset="1" style="stop-color:#38b0e3"/></linearGradient><circle cx="256" cy="256" r="256" style="fill:url(#telegram_svg__a)"/><path d="m173.3 274.7 30.4 84.1s3.8 7.9 7.9 7.9 64.5-62.9 64.5-62.9l67.3-129.9-169 79.1z" style="fill:#c8daea"/><path d="m213.6 296.3-5.8 62s-2.4 19 16.5 0c19-19 37.2-33.6 37.2-33.6" style="fill:#a9c6d8"/><path d="m173.8 277.7-62.5-20.4s-7.5-3-5.1-9.9c.5-1.4 1.5-2.6 4.5-4.7C124.6 233.1 367 146 367 146s6.8-2.3 10.9-.8c2 .6 3.6 2.3 4 4.4.4 1.8.6 3.7.5 5.5 0 1.6-.2 3.1-.4 5.4-1.5 23.8-45.7 201.6-45.7 201.6s-2.6 10.4-12.1 10.8c-4.7.2-9.3-1.6-12.6-4.9-18.6-16-82.8-59.2-97-68.6-.6-.4-1.1-1.1-1.2-1.9-.2-1 .9-2.2.9-2.2s111.8-99.4 114.8-109.8c.2-.8-.6-1.2-1.8-.9-7.4 2.7-136.2 84.1-150.4 93-.9.2-2 .3-3.1.1" style="fill:#fff"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -146,12 +146,6 @@ export class DocumentationService {
'pushover.sound': 'pushover.sound',
'pushover.tags': 'pushover.tags'
},
'notifications/telegram': {
'telegram.botToken': 'bot-token',
'telegram.chatId': 'chat-id',
'telegram.topicId': 'topic-id',
'telegram.sendSilently': 'send-silently'
},
};
constructor(private applicationPathService: ApplicationPathService) {}

View File

@@ -193,43 +193,6 @@ export interface TestPushoverProviderRequest {
tags: string[];
}
export interface CreateTelegramProviderRequest {
name: string;
isEnabled: boolean;
onFailedImportStrike: boolean;
onStalledStrike: boolean;
onSlowStrike: boolean;
onQueueItemDeleted: boolean;
onDownloadCleaned: boolean;
onCategoryChanged: boolean;
botToken: string;
chatId: string;
topicId: string;
sendSilently: boolean;
}
export interface UpdateTelegramProviderRequest {
name: string;
isEnabled: boolean;
onFailedImportStrike: boolean;
onStalledStrike: boolean;
onSlowStrike: boolean;
onQueueItemDeleted: boolean;
onDownloadCleaned: boolean;
onCategoryChanged: boolean;
botToken: string;
chatId: string;
topicId: string;
sendSilently: boolean;
}
export interface TestTelegramProviderRequest {
botToken: string;
chatId: string;
topicId: string;
sendSilently: boolean;
}
@Injectable({
providedIn: 'root'
})
@@ -280,13 +243,6 @@ export class NotificationProviderService {
return this.http.post<NotificationProviderDto>(`${this.baseUrl}/pushover`, provider);
}
/**
* Create a new Telegram provider
*/
createTelegramProvider(provider: CreateTelegramProviderRequest): Observable<NotificationProviderDto> {
return this.http.post<NotificationProviderDto>(`${this.baseUrl}/telegram`, provider);
}
/**
* Update an existing Notifiarr provider
*/
@@ -315,13 +271,6 @@ export class NotificationProviderService {
return this.http.put<NotificationProviderDto>(`${this.baseUrl}/pushover/${id}`, provider);
}
/**
* Update an existing Telegram provider
*/
updateTelegramProvider(id: string, provider: UpdateTelegramProviderRequest): Observable<NotificationProviderDto> {
return this.http.put<NotificationProviderDto>(`${this.baseUrl}/telegram/${id}`, provider);
}
/**
* Delete a notification provider
*/
@@ -357,13 +306,6 @@ export class NotificationProviderService {
return this.http.post<TestNotificationResult>(`${this.baseUrl}/pushover/test`, testRequest);
}
/**
* Test a Telegram provider (without ID - for testing configuration before saving)
*/
testTelegramProvider(testRequest: TestTelegramProviderRequest): Observable<TestNotificationResult> {
return this.http.post<TestNotificationResult>(`${this.baseUrl}/telegram/test`, testRequest);
}
/**
* Generic create method that delegates to provider-specific methods
*/
@@ -377,8 +319,6 @@ export class NotificationProviderService {
return this.createNtfyProvider(provider as CreateNtfyProviderRequest);
case NotificationProviderType.Pushover:
return this.createPushoverProvider(provider as CreatePushoverProviderRequest);
case NotificationProviderType.Telegram:
return this.createTelegramProvider(provider as CreateTelegramProviderRequest);
default:
throw new Error(`Unsupported provider type: ${type}`);
}
@@ -397,8 +337,6 @@ export class NotificationProviderService {
return this.updateNtfyProvider(id, provider as UpdateNtfyProviderRequest);
case NotificationProviderType.Pushover:
return this.updatePushoverProvider(id, provider as UpdatePushoverProviderRequest);
case NotificationProviderType.Telegram:
return this.updateTelegramProvider(id, provider as UpdateTelegramProviderRequest);
default:
throw new Error(`Unsupported provider type: ${type}`);
}
@@ -417,8 +355,6 @@ export class NotificationProviderService {
return this.testNtfyProvider(testRequest as TestNtfyProviderRequest);
case NotificationProviderType.Pushover:
return this.testPushoverProvider(testRequest as TestPushoverProviderRequest);
case NotificationProviderType.Telegram:
return this.testTelegramProvider(testRequest as TestTelegramProviderRequest);
default:
throw new Error(`Unsupported provider type: ${type}`);
}

View File

@@ -43,8 +43,8 @@
placeholder="My Notification Provider"
class="w-full"
/>
<small *ngIf="hasError('name', 'required')" class="form-error-text"> Provider name is required </small>
<small class="form-helper-text">A unique name to identify this provider</small>
<small *ngIf="hasError('name', 'required')" class="form-error-text"> Provider name is required </small>
</div>
<!-- Provider-Specific Configuration (Content Projection) -->

View File

@@ -50,13 +50,6 @@ export class ProviderTypeSelectionComponent {
iconUrl: 'icons/ext/pushover-light.svg',
iconUrlHover: 'icons/ext/pushover.svg',
description: 'https://pushover.net/'
},
{
type: NotificationProviderType.Telegram,
name: 'Telegram',
iconUrl: 'icons/ext/telegram-light.svg',
iconUrlHover: 'icons/ext/telegram.svg',
description: 'https://core.telegram.org/bots'
}
];

View File

@@ -1,92 +0,0 @@
<app-notification-provider-base
[visible]="visible"
modalTitle="Configure Telegram Provider"
[saving]="saving"
[testing]="testing"
[editingProvider]="editingProvider"
(save)="onSave($event)"
(cancel)="onCancel()"
(test)="onTest($event)">
<div slot="provider-config">
<div class="field">
<label for="bot-token">
<i
class="pi pi-question-circle field-info-icon"
title="Click for documentation"
(click)="openFieldDocs('telegram.botToken')"
></i>
Bot Token *
</label>
<input
id="bot-token"
type="password"
pInputText
[formControl]="botTokenControl"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
class="w-full"
/>
<small *ngIf="hasFieldError(botTokenControl, 'required')" class="form-error-text">Bot token is required</small>
<small *ngIf="hasFieldError(botTokenControl, 'minlength')" class="form-error-text">Bot token looks too short</small>
<small class="form-helper-text">Create a bot with BotFather and paste the API token</small>
</div>
<div class="field">
<label for="chat-id">
<i
class="pi pi-question-circle field-info-icon"
title="Click for documentation"
(click)="openFieldDocs('telegram.chatId')"
></i>
Chat ID *
</label>
<input
id="chat-id"
type="text"
pInputText
signedNumericInput
[formControl]="chatIdControl"
placeholder="e.g. 123456789 or -100123456789"
class="w-full"
/>
<small *ngIf="hasFieldError(chatIdControl, 'required')" class="form-error-text">Chat ID is required</small>
<small class="form-helper-text">Start a conversation with the bot or add it to your group to get the chat ID</small>
</div>
<div class="field">
<label for="topic-id">
<i
class="pi pi-question-circle field-info-icon"
title="Click for documentation"
(click)="openFieldDocs('telegram.topicId')"
></i>
Topic ID (optional)
</label>
<input
id="topic-id"
type="text"
pInputText
numericInput
[formControl]="topicIdControl"
placeholder="Enter topic ID for supergroup"
class="w-full"
/>
<small class="form-helper-text">Specify a Topic ID to send to a specific thread (supergroups only)</small>
</div>
<div class="field flex flex-row">
<label class="field-label">
<i
class="pi pi-question-circle field-info-icon"
title="Click for documentation"
(click)="openFieldDocs('telegram.sendSilently')"
></i>
Send Silently
</label>
<div class="field-input">
<p-checkbox [binary]="true" [formControl]="sendSilentlyControl"></p-checkbox>
<small class="form-helper-text">Deliver without sound for recipients</small>
</div>
</div>
</div>
</app-notification-provider-base>

View File

@@ -1 +0,0 @@
@use '../../../styles/settings-shared.scss';

View File

@@ -1,122 +0,0 @@
import { Component, Input, Output, EventEmitter, OnInit, OnChanges, SimpleChanges, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { InputTextModule } from 'primeng/inputtext';
import { CheckboxModule } from 'primeng/checkbox';
import { NotificationProviderBaseComponent } from '../base/notification-provider-base.component';
import { NumericInputDirective, SignedNumericInputDirective } from '../../../../shared/directives';
import { TelegramFormData, BaseProviderFormData } from '../../models/provider-modal.model';
import { NotificationProviderDto } from '../../../../shared/models/notification-provider.model';
import { DocumentationService } from '../../../../core/services/documentation.service';
@Component({
selector: 'app-telegram-provider',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
InputTextModule,
CheckboxModule,
NumericInputDirective,
SignedNumericInputDirective,
NotificationProviderBaseComponent
],
templateUrl: './telegram-provider.component.html',
styleUrls: ['./telegram-provider.component.scss']
})
export class TelegramProviderComponent implements OnInit, OnChanges {
@Input() visible = false;
@Input() editingProvider: NotificationProviderDto | null = null;
@Input() saving = false;
@Input() testing = false;
@Output() save = new EventEmitter<TelegramFormData>();
@Output() cancel = new EventEmitter<void>();
@Output() test = new EventEmitter<TelegramFormData>();
botTokenControl = new FormControl('', [Validators.required, Validators.minLength(10)]);
chatIdControl = new FormControl('', [Validators.required]);
topicIdControl = new FormControl('');
sendSilentlyControl = new FormControl(false);
private documentationService = inject(DocumentationService);
openFieldDocs(fieldName: string): void {
this.documentationService.openFieldDocumentation('notifications/telegram', fieldName);
}
ngOnInit(): void {
// initialization handled in ngOnChanges
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['editingProvider']) {
if (this.editingProvider) {
this.populateProviderFields();
} else {
this.resetProviderFields();
}
}
}
private populateProviderFields(): void {
if (!this.editingProvider) return;
const config = this.editingProvider.configuration as any;
this.botTokenControl.setValue(config?.botToken || '');
this.chatIdControl.setValue(config?.chatId || '');
this.topicIdControl.setValue(config?.topicId || '');
this.sendSilentlyControl.setValue(!!config?.sendSilently);
}
private resetProviderFields(): void {
this.botTokenControl.setValue('');
this.chatIdControl.setValue('');
this.topicIdControl.setValue('');
this.sendSilentlyControl.setValue(false);
}
protected hasFieldError(control: FormControl, errorType: string): boolean {
return !!(control && control.errors?.[errorType] && (control.dirty || control.touched));
}
private isFormValid(): boolean {
return this.botTokenControl.valid && this.chatIdControl.valid;
}
onSave(baseData: BaseProviderFormData): void {
if (this.isFormValid()) {
const telegramData: TelegramFormData = {
...baseData,
botToken: this.botTokenControl.value || '',
chatId: this.chatIdControl.value || '',
topicId: this.topicIdControl.value || '',
sendSilently: this.sendSilentlyControl.value || false,
};
this.save.emit(telegramData);
} else {
this.botTokenControl.markAsTouched();
this.chatIdControl.markAsTouched();
}
}
onCancel(): void {
this.cancel.emit();
}
onTest(baseData: BaseProviderFormData): void {
if (this.isFormValid()) {
const telegramData: TelegramFormData = {
...baseData,
botToken: this.botTokenControl.value || '',
chatId: this.chatIdControl.value || '',
topicId: this.topicIdControl.value || '',
sendSilently: this.sendSilentlyControl.value || false,
};
this.test.emit(telegramData);
} else {
this.botTokenControl.markAsTouched();
this.chatIdControl.markAsTouched();
}
}
}

View File

@@ -65,13 +65,6 @@ export interface PushoverFormData extends BaseProviderFormData {
tags: string[];
}
export interface TelegramFormData extends BaseProviderFormData {
botToken: string;
chatId: string;
topicId: string;
sendSilently: boolean;
}
// Events for modal communication
export interface ProviderModalEvents {
save: (data: any) => void;

View File

@@ -198,17 +198,6 @@
(test)="onPushoverTest($event)"
></app-pushover-provider>
<!-- Telegram Provider Modal -->
<app-telegram-provider
[visible]="showTelegramModal"
[editingProvider]="editingProvider"
[saving]="saving()"
[testing]="testing()"
(save)="onTelegramSave($event)"
(cancel)="onProviderCancel()"
(test)="onTelegramTest($event)"
></app-telegram-provider>
<!-- Confirmation Dialog -->
<p-confirmDialog></p-confirmDialog>

View File

@@ -8,7 +8,7 @@ import {
} from "../../shared/models/notification-provider.model";
import { NotificationProviderType } from "../../shared/models/enums";
import { DocumentationService } from "../../core/services/documentation.service";
import { NotifiarrFormData, AppriseFormData, NtfyFormData, PushoverFormData, TelegramFormData } from "./models/provider-modal.model";
import { NotifiarrFormData, AppriseFormData, NtfyFormData, PushoverFormData } from "./models/provider-modal.model";
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
// New modal components
@@ -17,7 +17,6 @@ import { NotifiarrProviderComponent } from "./modals/notifiarr-provider/notifiar
import { AppriseProviderComponent } from "./modals/apprise-provider/apprise-provider.component";
import { NtfyProviderComponent } from "./modals/ntfy-provider/ntfy-provider.component";
import { PushoverProviderComponent } from "./modals/pushover-provider/pushover-provider.component";
import { TelegramProviderComponent } from "./modals/telegram-provider/telegram-provider.component";
// PrimeNG Components
import { CardModule } from "primeng/card";
@@ -54,7 +53,6 @@ import { NotificationService } from "../../core/services/notification.service";
AppriseProviderComponent,
NtfyProviderComponent,
PushoverProviderComponent,
TelegramProviderComponent,
],
providers: [NotificationProviderConfigStore, ConfirmationService, MessageService],
templateUrl: "./notification-settings.component.html",
@@ -71,7 +69,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
showAppriseModal = false; // New: Apprise provider modal
showNtfyModal = false; // New: Ntfy provider modal
showPushoverModal = false; // New: Pushover provider modal
showTelegramModal = false; // New: Telegram provider modal
modalMode: 'add' | 'edit' = 'add';
editingProvider: NotificationProviderDto | null = null;
@@ -183,9 +180,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
case NotificationProviderType.Pushover:
this.showPushoverModal = true;
break;
case NotificationProviderType.Telegram:
this.showTelegramModal = true;
break;
default:
// For unsupported types, show the legacy modal with info message
this.showProviderModal = true;
@@ -239,9 +233,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
case NotificationProviderType.Pushover:
this.showPushoverModal = true;
break;
case NotificationProviderType.Telegram:
this.showTelegramModal = true;
break;
default:
// For unsupported types, show the legacy modal with info message
this.showProviderModal = true;
@@ -318,15 +309,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
tags: pushoverConfig.tags || [],
};
break;
case NotificationProviderType.Telegram:
const telegramConfig = provider.configuration as any;
testRequest = {
botToken: telegramConfig.botToken,
chatId: telegramConfig.chatId,
topicId: telegramConfig.topicId || "",
sendSilently: telegramConfig.sendSilently || false,
};
break;
default:
this.notificationService.showError("Testing not supported for this provider type");
return;
@@ -367,8 +349,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
return "ntfy";
case NotificationProviderType.Pushover:
return "Pushover";
case NotificationProviderType.Telegram:
return "Telegram";
default:
return "Unknown";
}
@@ -509,34 +489,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
});
}
/**
* Handle Telegram provider save
*/
onTelegramSave(data: TelegramFormData): void {
if (this.modalMode === "edit" && this.editingProvider) {
this.updateTelegramProvider(data);
} else {
this.createTelegramProvider(data);
}
}
/**
* Handle Telegram provider test
*/
onTelegramTest(data: TelegramFormData): void {
const testRequest = {
botToken: data.botToken,
chatId: data.chatId,
topicId: data.topicId,
sendSilently: data.sendSilently,
};
this.notificationProviderStore.testProvider({
testRequest,
type: NotificationProviderType.Telegram,
});
}
/**
* Handle provider modal cancel
*/
@@ -553,7 +505,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
this.showAppriseModal = false;
this.showNtfyModal = false;
this.showPushoverModal = false;
this.showTelegramModal = false;
this.showProviderModal = false;
this.editingProvider = null;
this.notificationProviderStore.clearTestResult();
@@ -793,61 +744,6 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
this.monitorProviderOperation("updated");
}
/**
* Create new Telegram provider
*/
private createTelegramProvider(data: TelegramFormData): void {
const createDto = {
name: data.name,
isEnabled: data.enabled,
onFailedImportStrike: data.onFailedImportStrike,
onStalledStrike: data.onStalledStrike,
onSlowStrike: data.onSlowStrike,
onQueueItemDeleted: data.onQueueItemDeleted,
onDownloadCleaned: data.onDownloadCleaned,
onCategoryChanged: data.onCategoryChanged,
botToken: data.botToken,
chatId: data.chatId,
topicId: data.topicId,
sendSilently: data.sendSilently,
};
this.notificationProviderStore.createProvider({
provider: createDto,
type: NotificationProviderType.Telegram,
});
this.monitorProviderOperation("created");
}
/**
* Update existing Telegram provider
*/
private updateTelegramProvider(data: TelegramFormData): void {
if (!this.editingProvider) return;
const updateDto = {
name: data.name,
isEnabled: data.enabled,
onFailedImportStrike: data.onFailedImportStrike,
onStalledStrike: data.onStalledStrike,
onSlowStrike: data.onSlowStrike,
onQueueItemDeleted: data.onQueueItemDeleted,
onDownloadCleaned: data.onDownloadCleaned,
onCategoryChanged: data.onCategoryChanged,
botToken: data.botToken,
chatId: data.chatId,
topicId: data.topicId,
sendSilently: data.sendSilently,
};
this.notificationProviderStore.updateProvider({
id: this.editingProvider.id,
provider: updateDto,
type: NotificationProviderType.Telegram,
});
this.monitorProviderOperation("updated");
}
/**
* Monitor provider operation completion and close modals
*/

View File

@@ -1,2 +1 @@
export * from './numeric-input.directive';
export * from './signed-numeric-input.directive';

View File

@@ -38,11 +38,11 @@ export class NumericInputDirective {
onKeyDown(event: KeyboardEvent): void {
// Allow: backspace, delete, tab, escape, enter
if ([8, 9, 27, 13, 46].indexOf(event.keyCode) !== -1 ||
// Allow: Ctrl/Cmd+A,C,V,X
(event.keyCode === 65 && (event.ctrlKey === true || event.metaKey === true)) ||
(event.keyCode === 67 && (event.ctrlKey === true || event.metaKey === true)) ||
(event.keyCode === 86 && (event.ctrlKey === true || event.metaKey === true)) ||
(event.keyCode === 88 && (event.ctrlKey === true || event.metaKey === true)) ||
// Allow: Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
(event.keyCode === 65 && event.ctrlKey === true) ||
(event.keyCode === 67 && event.ctrlKey === true) ||
(event.keyCode === 86 && event.ctrlKey === true) ||
(event.keyCode === 88 && event.ctrlKey === true) ||
// Allow: home, end, left, right
(event.keyCode >= 35 && event.keyCode <= 39)) {
return;

View File

@@ -1,87 +0,0 @@
import { Directive, HostListener } from '@angular/core';
import { NgControl } from '@angular/forms';
/**
* Directive that restricts input to numeric characters with an optional leading minus sign.
* Useful for Telegram chat IDs which can be negative for groups/supergroups.
*/
@Directive({
selector: '[signedNumericInput]',
standalone: true
})
export class SignedNumericInputDirective {
constructor(private ngControl: NgControl) {}
@HostListener('input', ['$event'])
onInput(event: Event): void {
const input = event.target as HTMLInputElement;
const originalValue = input.value;
const sanitized = this.sanitize(originalValue);
if (sanitized !== originalValue) {
input.value = sanitized;
this.ngControl.control?.setValue(sanitized);
}
}
@HostListener('keydown', ['$event'])
onKeyDown(event: KeyboardEvent): void {
// Allow: backspace, delete, tab, escape, enter
if ([8, 9, 27, 13, 46].includes(event.keyCode) ||
// Allow: Ctrl/Cmd+A,C,V,X
(event.keyCode === 65 && (event.ctrlKey || event.metaKey)) ||
(event.keyCode === 67 && (event.ctrlKey || event.metaKey)) ||
(event.keyCode === 86 && (event.ctrlKey || event.metaKey)) ||
(event.keyCode === 88 && (event.ctrlKey || event.metaKey)) ||
// Allow: home, end, left, right
(event.keyCode >= 35 && event.keyCode <= 39)) {
return;
}
// Allow minus only at the start and only if not already present
if ((event.key === '-' || event.keyCode === 189 || event.keyCode === 109)) {
const input = event.target as HTMLInputElement;
const hasMinus = input.value.includes('-');
const cursorAtStart = (input.selectionStart ?? 0) === 0;
if (!hasMinus && cursorAtStart) {
return;
}
event.preventDefault();
return;
}
// Block non-numeric keys
if ((event.shiftKey || (event.keyCode < 48 || event.keyCode > 57)) && (event.keyCode < 96 || event.keyCode > 105)) {
event.preventDefault();
}
}
@HostListener('paste', ['$event'])
onPaste(event: ClipboardEvent): void {
const pasted = event.clipboardData?.getData('text') || '';
const sanitized = this.sanitize(pasted);
if (sanitized !== pasted) {
event.preventDefault();
const input = event.target as HTMLInputElement;
const currentValue = input.value;
const start = input.selectionStart ?? 0;
const end = input.selectionEnd ?? 0;
const newValue = currentValue.substring(0, start) + sanitized + currentValue.substring(end);
input.value = newValue;
this.ngControl.control?.setValue(newValue);
const cursor = start + sanitized.length;
setTimeout(() => input.setSelectionRange(cursor, cursor));
}
}
private sanitize(value: string): string {
if (!value) return '';
const hasMinus = value.startsWith('-');
const digits = value.replace(/\D/g, '');
return hasMinus ? `-${digits}` : digits;
}
}

View File

@@ -15,7 +15,6 @@ export enum NotificationProviderType {
Apprise = "Apprise",
Ntfy = "Ntfy",
Pushover = "Pushover",
Telegram = "Telegram",
}
export enum AppriseMode {

View File

@@ -63,10 +63,3 @@ export interface AppriseConfiguration {
export interface TestNotificationResult {
message: string;
}
export interface TelegramConfiguration {
botToken: string;
chatId: string;
topicId?: string;
sendSilently: boolean;
}

View File

@@ -2,6 +2,7 @@
sidebar_position: 3
---
import { Important } from '@site/src/components/documentation';
import {
AppCard,
styles
@@ -10,7 +11,11 @@ import useBaseUrl from '@docusaurus/useBaseUrl';
# Supported Apps
Cleanuparr integrates with popular *arr applications and download clients for media management automation.
Cleanuparr integrates with the servarr applications and download clients for media management automation.
<Important>
Only the latest versions of the following applications are supported, unless explicitly specified otherwise.
</Important>
<div className={styles.documentationPage}>

View File

@@ -1,78 +0,0 @@
---
sidebar_position: 5
---
import {
ConfigSection,
ElementNavigator,
SectionTitle,
styles
} from '@site/src/components/documentation';
# Telegram
Telegram can deliver notifications to users, groups, or supergroups via bots.
<ElementNavigator />
<div className={styles.documentationPage}>
<div className={styles.section}>
<SectionTitle icon="🤖">Configuration</SectionTitle>
<p className={styles.sectionDescription}>
Configure a Telegram bot to send notifications to a chat (user, group, or supergroup). Chat IDs can be negative for groups/supergroups; topic IDs are for threads in supergroups.
</p>
<ConfigSection
title="Bot Token"
icon="🔑"
id="bot-token"
>
Create a bot with [@BotFather](https://t.me/BotFather) and paste the generated token. Tokens look like `123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`.
<br/>
Reference: https://core.telegram.org/bots#how-do-i-create-a-bot
</ConfigSection>
<ConfigSection
title="Chat ID"
icon="💬"
id="chat-id"
>
The destination chat. Examples:
- Direct chat with your bot: positive integer (e.g., `123456789`)
- Group/supergroup: negative integer starting with `-100` (e.g., `-100123456789`)
One way to find the chat id: https://stackoverflow.com/a/75954034
</ConfigSection>
<ConfigSection
title="Topic ID (optional)"
icon="🧵"
id="topic-id"
>
For supergroups with topics enabled, specify the thread/topic ID to target a specific thread. Leave empty to post to the main chat.
<br/>
One way to get the topic id: https://stackoverflow.com/a/75178418
</ConfigSection>
<ConfigSection
title="Send Silently"
icon="🔕"
id="send-silently"
>
When enabled, Telegram delivers the message without sound.
</ConfigSection>
</div>
</div>