mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-30 01:18:36 -05:00
Compare commits
6 Commits
update_pac
...
add_telegr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e390ef888 | ||
|
|
4bb3e24fc0 | ||
|
|
b0fdf21f2b | ||
|
|
3af8cfcc5c | ||
|
|
55c4d269d8 | ||
|
|
453acc4dda |
@@ -3,6 +3,7 @@ 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;
|
||||
|
||||
@@ -16,6 +17,7 @@ public static class NotificationsDI
|
||||
.AddSingleton<IAppriseCliDetector, AppriseCliDetector>()
|
||||
.AddScoped<INtfyProxy, NtfyProxy>()
|
||||
.AddScoped<IPushoverProxy, PushoverProxy>()
|
||||
.AddScoped<ITelegramProxy, TelegramProxy>()
|
||||
.AddScoped<INotificationConfigurationService, NotificationConfigurationService>()
|
||||
.AddScoped<INotificationProviderFactory, NotificationProviderFactory>()
|
||||
.AddScoped<NotificationProviderFactory>()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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; }
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -48,6 +49,7 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
.Include(p => p.AppriseConfiguration)
|
||||
.Include(p => p.NtfyConfiguration)
|
||||
.Include(p => p.PushoverConfiguration)
|
||||
.Include(p => p.TelegramConfiguration)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
@@ -73,6 +75,7 @@ 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()
|
||||
}
|
||||
})
|
||||
@@ -289,6 +292,69 @@ 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)
|
||||
{
|
||||
@@ -535,6 +601,87 @@ 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)
|
||||
{
|
||||
@@ -546,6 +693,7 @@ 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)
|
||||
@@ -707,6 +855,53 @@ 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
|
||||
@@ -730,6 +925,7 @@ 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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,5 +5,6 @@ public enum NotificationProviderType
|
||||
Notifiarr,
|
||||
Apprise,
|
||||
Ntfy,
|
||||
Pushover
|
||||
Pushover,
|
||||
Telegram,
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ public sealed class NotificationConfigurationService : INotificationConfiguratio
|
||||
.Include(p => p.AppriseConfiguration)
|
||||
.Include(p => p.NtfyConfiguration)
|
||||
.Include(p => p.PushoverConfiguration)
|
||||
.Include(p => p.TelegramConfiguration)
|
||||
.AsNoTracking()
|
||||
.ToListAsync();
|
||||
|
||||
@@ -137,6 +138,7 @@ 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")
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
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;
|
||||
|
||||
@@ -26,6 +28,7 @@ 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")
|
||||
};
|
||||
}
|
||||
@@ -62,4 +65,12 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Telegram;
|
||||
|
||||
public interface ITelegramProxy
|
||||
{
|
||||
Task SendNotification(TelegramPayload payload, string botToken);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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)}\">​</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];
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ 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; }
|
||||
|
||||
@@ -149,6 +151,11 @@ 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();
|
||||
});
|
||||
|
||||
|
||||
1161
code/backend/Cleanuparr.Persistence/Migrations/Data/20251227170911_AddTelegram.Designer.cs
generated
Normal file
1161
code/backend/Cleanuparr.Persistence/Migrations/Data/20251227170911_AddTelegram.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -738,6 +738,48 @@ 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")
|
||||
@@ -1033,6 +1075,18 @@ 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")
|
||||
@@ -1088,6 +1142,8 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
b.Navigation("NtfyConfiguration");
|
||||
|
||||
b.Navigation("PushoverConfiguration");
|
||||
|
||||
b.Navigation("TelegramConfiguration");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
|
||||
|
||||
@@ -43,6 +43,8 @@ public sealed record NotificationConfig
|
||||
|
||||
public PushoverConfig? PushoverConfiguration { get; init; }
|
||||
|
||||
public TelegramConfig? TelegramConfiguration { get; init; }
|
||||
|
||||
[NotMapped]
|
||||
public bool IsConfigured => Type switch
|
||||
{
|
||||
@@ -50,6 +52,7 @@ 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}")
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
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 _);
|
||||
}
|
||||
}
|
||||
1
code/frontend/public/icons/ext/telegram-light.svg
Normal file
1
code/frontend/public/icons/ext/telegram-light.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 864 B |
1
code/frontend/public/icons/ext/telegram.svg
Normal file
1
code/frontend/public/icons/ext/telegram.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -146,6 +146,12 @@ 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) {}
|
||||
|
||||
@@ -193,6 +193,43 @@ 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'
|
||||
})
|
||||
@@ -243,6 +280,13 @@ 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
|
||||
*/
|
||||
@@ -271,6 +315,13 @@ 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
|
||||
*/
|
||||
@@ -306,6 +357,13 @@ 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
|
||||
*/
|
||||
@@ -319,6 +377,8 @@ 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}`);
|
||||
}
|
||||
@@ -337,6 +397,8 @@ 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}`);
|
||||
}
|
||||
@@ -355,6 +417,8 @@ 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}`);
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
placeholder="My Notification Provider"
|
||||
class="w-full"
|
||||
/>
|
||||
<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>
|
||||
<small class="form-helper-text">A unique name to identify this provider</small>
|
||||
</div>
|
||||
|
||||
<!-- Provider-Specific Configuration (Content Projection) -->
|
||||
|
||||
@@ -50,6 +50,13 @@ 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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<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>
|
||||
@@ -0,0 +1 @@
|
||||
@use '../../../styles/settings-shared.scss';
|
||||
@@ -0,0 +1,122 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,13 @@ 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;
|
||||
|
||||
@@ -198,6 +198,17 @@
|
||||
(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>
|
||||
|
||||
|
||||
@@ -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 } from "./models/provider-modal.model";
|
||||
import { NotifiarrFormData, AppriseFormData, NtfyFormData, PushoverFormData, TelegramFormData } from "./models/provider-modal.model";
|
||||
import { LoadingErrorStateComponent } from "../../shared/components/loading-error-state/loading-error-state.component";
|
||||
|
||||
// New modal components
|
||||
@@ -17,6 +17,7 @@ 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";
|
||||
@@ -53,6 +54,7 @@ import { NotificationService } from "../../core/services/notification.service";
|
||||
AppriseProviderComponent,
|
||||
NtfyProviderComponent,
|
||||
PushoverProviderComponent,
|
||||
TelegramProviderComponent,
|
||||
],
|
||||
providers: [NotificationProviderConfigStore, ConfirmationService, MessageService],
|
||||
templateUrl: "./notification-settings.component.html",
|
||||
@@ -69,6 +71,7 @@ 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;
|
||||
|
||||
@@ -180,6 +183,9 @@ 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;
|
||||
@@ -233,6 +239,9 @@ 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;
|
||||
@@ -309,6 +318,15 @@ 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;
|
||||
@@ -349,6 +367,8 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
return "ntfy";
|
||||
case NotificationProviderType.Pushover:
|
||||
return "Pushover";
|
||||
case NotificationProviderType.Telegram:
|
||||
return "Telegram";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -489,6 +509,34 @@ 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
|
||||
*/
|
||||
@@ -505,6 +553,7 @@ 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();
|
||||
@@ -744,6 +793,61 @@ 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
|
||||
*/
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './numeric-input.directive';
|
||||
export * from './signed-numeric-input.directive';
|
||||
|
||||
@@ -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+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: 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: home, end, left, right
|
||||
(event.keyCode >= 35 && event.keyCode <= 39)) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export enum NotificationProviderType {
|
||||
Apprise = "Apprise",
|
||||
Ntfy = "Ntfy",
|
||||
Pushover = "Pushover",
|
||||
Telegram = "Telegram",
|
||||
}
|
||||
|
||||
export enum AppriseMode {
|
||||
|
||||
@@ -63,3 +63,10 @@ export interface AppriseConfiguration {
|
||||
export interface TestNotificationResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TelegramConfiguration {
|
||||
botToken: string;
|
||||
chatId: string;
|
||||
topicId?: string;
|
||||
sendSilently: boolean;
|
||||
}
|
||||
|
||||
78
docs/docs/configuration/notifications/telegram.mdx
Normal file
78
docs/docs/configuration/notifications/telegram.mdx
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user