feat(notifications): optional Telegram forum topic via message_thread_id (#1518)

Telegram groups with Topics enabled always received notifications in the
General topic, since only Bot Token and Chat ID were configurable. Splitting
notifications per printer meant running a separate chat for each one.

The Telegram provider now takes an optional Forum Topic ID - the last number
in a topic's link, t.me/c/1234567890/25 - and routes its messages there. Left
empty, nothing changes.

The value is coerced to an int once in _send_telegram and attached to both the
sendMessage JSON body and the sendPhoto form data. That ordering matters:
Telegram rejects a string message_thread_id in the JSON body while accepting
one in the multipart call, so passing the raw form value through would have
worked for thumbnail notifications and 400'd for plain-text ones. A
non-numeric value is rejected in the form and again server-side before any
request goes out.

No migration - provider config is a JSON blob.

Adds Forum Topic ID plus help text to the Telegram section of the provider
dialog, translated in all 13 locales. Backend tests cover omitted / blank /
int-typed / non-numeric values and both send paths; frontend tests cover the
field being optional, absent for other providers, round-tripping on save, and
blocking save on a bad value.
This commit is contained in:
maziggy
2026-07-29 14:10:38 +02:00
parent c11bcecfd8
commit 6cda236dce
20 changed files with 294 additions and 17 deletions
@@ -160,6 +160,15 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
}
}
// Telegram forum topic must be a plain integer (#1518) — type="number"
// still lets "1e5" and "-" through, and Telegram would 400 on those.
if (providerType === 'telegram' && config.message_thread_id?.trim()) {
if (!/^\d+$/.test(config.message_thread_id.trim())) {
setError(t('notifications.telegramThreadIdInvalid'));
return;
}
}
const finalConfig: Record<string, unknown> =
providerType === 'ntfy' && Object.keys(eventPriorities).length > 0
? { ...config, event_priorities: eventPriorities }
@@ -245,6 +254,16 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
return [
{ key: 'bot_token', label: 'Bot Token', placeholder: 'Bot token from @BotFather', type: 'password', required: true },
{ key: 'chat_id', label: 'Chat ID', placeholder: 'Your chat or group ID', type: 'text', required: true },
// Optional forum topic (#1518). Left empty, Telegram posts to the
// group's General topic exactly as before.
{
key: 'message_thread_id',
label: t('notifications.telegramThreadId'),
placeholder: '123',
type: 'number',
required: false,
help: t('notifications.telegramThreadIdHelp'),
},
];
case 'email':
return [
@@ -423,6 +442,9 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
/>
)}
{'help' in field && (field as { help?: string }).help && (
<p className="text-xs text-bambu-gray mt-1">{(field as { help?: string }).help}</p>
)}
</div>
))}
</div>