feat: add Telegram notification provider support

- Updated API to support Telegram as a notification provider type.
- Enhanced tests to cover Telegram provider creation, updates, and token handling.
- Modified frontend forms to include Telegram-specific fields and validation.
- Added localization strings for Telegram provider.
- Implemented security measures to ensure bot tokens are not exposed in API responses.
This commit is contained in:
GitHub Actions
2026-03-10 12:14:57 +00:00
parent 317bff326b
commit ef71f66029
26 changed files with 1884 additions and 366 deletions
+11 -9
View File
@@ -22,7 +22,7 @@ const isSupportedProviderType = (providerType: string | undefined): providerType
const supportsJSONTemplates = (providerType: string | undefined): boolean => {
if (!providerType) return false;
const t = providerType.toLowerCase();
return t === 'discord' || t === 'gotify' || t === 'webhook';
return t === 'discord' || t === 'gotify' || t === 'webhook' || t === 'telegram';
};
const isUnsupportedProviderType = (providerType: string | undefined): boolean => !isSupportedProviderType(providerType);
@@ -42,7 +42,7 @@ const normalizeProviderPayloadForSubmit = (data: Partial<NotificationProvider>):
type,
};
if (type === 'gotify') {
if (type === 'gotify' || type === 'telegram') {
const normalizedToken = typeof payload.gotify_token === 'string' ? payload.gotify_token.trim() : '';
if (normalizedToken.length > 0) {
@@ -139,9 +139,10 @@ const ProviderForm: FC<{
const type = normalizeProviderType(watch('type'));
const isGotify = type === 'gotify';
const isTelegram = type === 'telegram';
const isEmail = type === 'email';
useEffect(() => {
if (type !== 'gotify') {
if (type !== 'gotify' && type !== 'telegram') {
setValue('gotify_token', '', { shouldDirty: false, shouldTouch: false });
}
}, [type, setValue]);
@@ -196,12 +197,13 @@ const ProviderForm: FC<{
<option value="gotify">Gotify</option>
<option value="webhook">{t('notificationProviders.genericWebhook')}</option>
<option value="email">Email</option>
<option value="telegram">{t('notificationProviders.telegram')}</option>
</select>
</div>
<div>
<label htmlFor="provider-url" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{isEmail ? t('notificationProviders.recipients') : <>{t('notificationProviders.urlWebhook')} <span aria-hidden="true">*</span></>}
{isEmail ? t('notificationProviders.recipients') : isTelegram ? t('notificationProviders.telegramChatId') : <>{t('notificationProviders.urlWebhook')} <span aria-hidden="true">*</span></>}
</label>
{isEmail && (
<p id="email-recipients-help" className="text-xs text-gray-500 mt-0.5">
@@ -212,10 +214,10 @@ const ProviderForm: FC<{
id="provider-url"
{...register('url', {
required: isEmail ? false : (t('notificationProviders.urlRequired') as string),
validate: isEmail ? undefined : validateUrl,
validate: (isEmail || isTelegram) ? undefined : validateUrl,
})}
data-testid="provider-url"
placeholder={isEmail ? 'user@example.com, admin@example.com' : type === 'discord' ? 'https://discord.com/api/webhooks/...' : type === 'gotify' ? 'https://gotify.example.com/message' : 'https://example.com/webhook'}
placeholder={isEmail ? 'user@example.com, admin@example.com' : isTelegram ? '987654321' : type === 'discord' ? 'https://discord.com/api/webhooks/...' : type === 'gotify' ? 'https://gotify.example.com/message' : 'https://example.com/webhook'}
className={`mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm ${errors.url ? 'border-red-500' : ''}`}
aria-invalid={errors.url ? 'true' : 'false'}
aria-describedby={isEmail ? 'email-recipients-help' : errors.url ? 'provider-url-error' : undefined}
@@ -235,10 +237,10 @@ const ProviderForm: FC<{
</div>
)}
{isGotify && (
{(isGotify || isTelegram) && (
<div>
<label htmlFor="provider-gotify-token" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{t('notificationProviders.gotifyToken')}
{isTelegram ? t('notificationProviders.telegramBotToken') : t('notificationProviders.gotifyToken')}
</label>
<input
id="provider-gotify-token"
@@ -246,7 +248,7 @@ const ProviderForm: FC<{
autoComplete="new-password"
{...register('gotify_token')}
data-testid="provider-gotify-token"
placeholder={initialData?.has_token ? t('notificationProviders.gotifyTokenKeepPlaceholder') : t('notificationProviders.gotifyTokenPlaceholder')}
placeholder={initialData?.has_token ? t('notificationProviders.gotifyTokenKeepPlaceholder') : isTelegram ? t('notificationProviders.telegramBotTokenPlaceholder') : t('notificationProviders.gotifyTokenPlaceholder')}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white sm:text-sm"
aria-describedby={initialData?.has_token ? 'gotify-token-stored-hint' : undefined}
/>
@@ -14,7 +14,7 @@ vi.mock('react-i18next', () => ({
}))
vi.mock('../../api/notifications', () => ({
SUPPORTED_NOTIFICATION_PROVIDER_TYPES: ['discord', 'gotify', 'webhook', 'email'],
SUPPORTED_NOTIFICATION_PROVIDER_TYPES: ['discord', 'gotify', 'webhook', 'email', 'telegram'],
getProviders: vi.fn(),
createProvider: vi.fn(),
updateProvider: vi.fn(),
@@ -146,8 +146,8 @@ describe('Notifications', () => {
const typeSelect = screen.getByTestId('provider-type') as HTMLSelectElement
const options = Array.from(typeSelect.options)
expect(options).toHaveLength(4)
expect(options.map((option) => option.value)).toEqual(['discord', 'gotify', 'webhook', 'email'])
expect(options).toHaveLength(5)
expect(options.map((option) => option.value)).toEqual(['discord', 'gotify', 'webhook', 'email', 'telegram'])
expect(typeSelect.disabled).toBe(false)
})