mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-30 09:28:22 -05:00
Compare commits
3 Commits
add_telegr
...
update_pac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
605c109de2 | ||
|
|
399a363090 | ||
|
|
2270a0c6bf |
2
.github/workflows/build-executable.yml
vendored
2
.github/workflows/build-executable.yml
vendored
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Setup dotnet
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
|
||||
2
.github/workflows/build-macos-installer.yml
vendored
2
.github/workflows/build-macos-installer.yml
vendored
@@ -86,7 +86,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Restore .NET dependencies
|
||||
run: |
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Restore .NET dependencies
|
||||
run: |
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 9.0.x
|
||||
dotnet-version: 10.0.x
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -19,7 +19,7 @@ This helps us avoid redundant work, git conflicts, and contributions that may no
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)
|
||||
- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
|
||||
- [Node.js 18+](https://nodejs.org/)
|
||||
- [Git](https://git-scm.com/)
|
||||
- (Optional) [Make](https://www.gnu.org/software/make/) for database migrations
|
||||
|
||||
@@ -15,7 +15,7 @@ COPY frontend/ .
|
||||
RUN npm run build
|
||||
|
||||
# Build .NET backend
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:9.0-bookworm-slim AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG VERSION=0.0.1
|
||||
ARG PACKAGES_USERNAME
|
||||
@@ -42,7 +42,7 @@ RUN --mount=type=cache,target=/root/.nuget/packages,sharing=locked \
|
||||
/p:DebugSymbols=false
|
||||
|
||||
# Runtime stage
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-bookworm-slim
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
|
||||
# Install required packages for user management, timezone support, and Python for Apprise CLI
|
||||
RUN apt-get update && apt-get install -y \
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<AssemblyName>Cleanuparr</AssemblyName>
|
||||
<Version Condition="'$(Version)' == ''">0.0.1</Version>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
@@ -24,14 +24,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MassTransit" Version="8.5.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.1" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -5,6 +5,5 @@ public enum NotificationProviderType
|
||||
Notifiarr,
|
||||
Apprise,
|
||||
Ntfy,
|
||||
Pushover,
|
||||
Telegram,
|
||||
Pushover
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -19,9 +19,9 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
@@ -31,7 +31,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -13,15 +13,15 @@
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="MassTransit.Abstractions" Version="8.5.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||
<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="5.0.0" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="10.0.0" />
|
||||
<PackageReference Include="System.Threading.RateLimiting" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Notifications.Telegram;
|
||||
|
||||
public interface ITelegramProxy
|
||||
{
|
||||
Task SendNotification(TelegramPayload payload, string botToken);
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)}\">​</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];
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -22,7 +22,7 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
@@ -11,9 +11,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="10.0.0-rc.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.1" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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}")
|
||||
};
|
||||
|
||||
|
||||
@@ -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 _);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"angular-eslint"
|
||||
]
|
||||
],
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
|
||||
806
code/frontend/package-lock.json
generated
806
code/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,14 +11,16 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^19.2.16",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.16",
|
||||
"@angular/forms": "^19.2.16",
|
||||
"@angular/platform-browser": "^19.2.16",
|
||||
"@angular/platform-browser-dynamic": "^19.2.16",
|
||||
"@angular/router": "^19.2.16",
|
||||
"@angular/service-worker": "^19.2.16",
|
||||
"@angular/animations": "^19.2.17",
|
||||
"@angular/cdk": "^19.2.17",
|
||||
"@angular/common": "^19.2.17",
|
||||
"@angular/compiler": "^19.2.17",
|
||||
"@angular/core": "^19.2.17",
|
||||
"@angular/forms": "^19.2.17",
|
||||
"@angular/platform-browser": "^19.2.17",
|
||||
"@angular/platform-browser-dynamic": "^19.2.17",
|
||||
"@angular/router": "^19.2.17",
|
||||
"@angular/service-worker": "^19.2.17",
|
||||
"@microsoft/signalr": "^8.0.7",
|
||||
"@ngrx/signals": "^19.2.0",
|
||||
"@primeng/themes": "^19.1.3",
|
||||
@@ -30,9 +32,9 @@
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.12",
|
||||
"@angular/cli": "^19.2.12",
|
||||
"@angular/compiler-cli": "^19.2.16",
|
||||
"@angular-devkit/build-angular": "^19.2.17",
|
||||
"@angular/cli": "^19.2.17",
|
||||
"@angular/compiler-cli": "^19.2.17",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"angular-eslint": "19.6.0",
|
||||
"eslint": "^9.27.0",
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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) {}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
@use '../../../styles/settings-shared.scss';
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
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/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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ export enum NotificationProviderType {
|
||||
Apprise = "Apprise",
|
||||
Ntfy = "Ntfy",
|
||||
Pushover = "Pushover",
|
||||
Telegram = "Telegram",
|
||||
}
|
||||
|
||||
export enum AppriseMode {
|
||||
|
||||
@@ -63,10 +63,3 @@ export interface AppriseConfiguration {
|
||||
export interface TestNotificationResult {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface TelegramConfiguration {
|
||||
botToken: string;
|
||||
chatId: string;
|
||||
topicId?: string;
|
||||
sendSilently: boolean;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -39,16 +39,16 @@ Download and configure the .NET SDK for FreeBSD:
|
||||
cd ~
|
||||
|
||||
# Set up variables for cleaner commands
|
||||
DOTNET_VERSION="v9.0.104-amd64-freebsd-14"
|
||||
DOTNET_VERSION="v10.0.101-amd64-freebsd-14"
|
||||
DOTNET_BASE_URL="https://github.com/Thefrank/dotnet-freebsd-crossbuild/releases/download"
|
||||
|
||||
# Download .NET SDK
|
||||
wget -q "${DOTNET_BASE_URL}/${DOTNET_VERSION}/dotnet-sdk-9.0.104-freebsd-x64.tar.gz"
|
||||
wget -q "${DOTNET_BASE_URL}/${DOTNET_VERSION}/dotnet-sdk-10.0.101-freebsd-x64.tar.gz"
|
||||
|
||||
# Set up .NET environment
|
||||
export DOTNET_ROOT=$(pwd)/.dotnet
|
||||
mkdir -p "$DOTNET_ROOT"
|
||||
tar zxf dotnet-sdk-9.0.104-freebsd-x64.tar.gz -C "$DOTNET_ROOT"
|
||||
tar zxf dotnet-sdk-10.0.101-freebsd-x64.tar.gz -C "$DOTNET_ROOT"
|
||||
export PATH=$PATH:$DOTNET_ROOT:$DOTNET_ROOT/tools
|
||||
```
|
||||
</Step>
|
||||
@@ -70,7 +70,7 @@ mkdir -p /tmp/nuget
|
||||
|
||||
# Set up variables for package URLs
|
||||
NUGET_BASE_URL="${DOTNET_BASE_URL}/${DOTNET_VERSION}"
|
||||
RUNTIME_VERSION="9.0.3"
|
||||
RUNTIME_VERSION="10.0.1"
|
||||
|
||||
# Download required packages
|
||||
wget -q -P /tmp/nuget/ \
|
||||
|
||||
37
docs/package-lock.json
generated
37
docs/package-lock.json
generated
@@ -220,6 +220,7 @@
|
||||
"version": "5.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.41.0.tgz",
|
||||
"integrity": "sha512-G9I2atg1ShtFp0t7zwleP6aPS4DcZvsV4uoQOripp16aR6VJzbEnKFPLW4OFXzX7avgZSpYeBAS+Zx4FOgmpPw==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "5.41.0",
|
||||
"@algolia/requester-browser-xhr": "5.41.0",
|
||||
@@ -335,6 +336,7 @@
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
|
||||
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.3",
|
||||
@@ -2015,6 +2017,7 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@@ -2036,6 +2039,7 @@
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@@ -2140,6 +2144,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -2545,6 +2550,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -3389,6 +3395,7 @@
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz",
|
||||
"integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.9.2",
|
||||
"@docusaurus/logger": "3.9.2",
|
||||
@@ -4105,6 +4112,7 @@
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz",
|
||||
"integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/mdx": "^2.0.0"
|
||||
},
|
||||
@@ -4397,6 +4405,7 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
|
||||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
@@ -4730,6 +4739,7 @@
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -5042,6 +5052,7 @@
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -5109,6 +5120,7 @@
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -5151,6 +5163,7 @@
|
||||
"version": "5.41.0",
|
||||
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.41.0.tgz",
|
||||
"integrity": "sha512-9E4b3rJmYbBkn7e3aAPt1as+VVnRhsR4qwRRgOzpeyz4PAOuwKh0HI4AN6mTrqK0S0M9fCCSTOUnuJ8gPY/tvA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@algolia/abtesting": "1.7.0",
|
||||
"@algolia/client-abtesting": "5.41.0",
|
||||
@@ -5691,6 +5704,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -6613,6 +6627,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -7915,6 +7930,7 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -10031,9 +10047,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
||||
"version": "13.2.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
|
||||
"integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
@@ -12130,6 +12147,7 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -12610,6 +12628,7 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -13465,6 +13484,7 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz",
|
||||
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
@@ -14261,6 +14281,7 @@
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -14269,6 +14290,7 @@
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -14319,6 +14341,7 @@
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz",
|
||||
"integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
},
|
||||
@@ -14371,6 +14394,7 @@
|
||||
"version": "5.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz",
|
||||
"integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
@@ -16141,7 +16165,8 @@
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
@@ -16209,6 +16234,7 @@
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -16526,6 +16552,7 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -16713,6 +16740,7 @@
|
||||
"version": "5.99.5",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz",
|
||||
"integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
@@ -17287,6 +17315,7 @@
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -4668,6 +4668,11 @@ fs-extra@^11.1.1, fs-extra@^11.2.0:
|
||||
jsonfile "^6.0.1"
|
||||
universalify "^2.0.0"
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
|
||||
@@ -5896,9 +5901,9 @@ mdast-util-phrasing@^4.0.0:
|
||||
unist-util-is "^6.0.0"
|
||||
|
||||
mdast-util-to-hast@^13.0.0:
|
||||
version "13.2.0"
|
||||
resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz"
|
||||
integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
|
||||
version "13.2.1"
|
||||
resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz"
|
||||
integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==
|
||||
dependencies:
|
||||
"@types/hast" "^3.0.0"
|
||||
"@types/mdast" "^4.0.0"
|
||||
|
||||
Reference in New Issue
Block a user