diff --git a/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go b/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go index a46c036..174a4dc 100644 --- a/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go +++ b/backend/internal/features/backups/backups/usecases/postgresql/create_backup_uc.go @@ -71,7 +71,8 @@ func (uc *CreatePostgresqlBackupUsecase) Execute( // Use zstd compression level 5 for PostgreSQL 15+ (better compression and speed) // Fall back to gzip compression level 5 for older versions - if pg.Version == tools.PostgresqlVersion13 || pg.Version == tools.PostgresqlVersion14 || pg.Version == tools.PostgresqlVersion15 { + if pg.Version == tools.PostgresqlVersion13 || pg.Version == tools.PostgresqlVersion14 || + pg.Version == tools.PostgresqlVersion15 { args = append(args, "-Z", "5") uc.logger.Info("Using gzip compression level 5 (zstd not available)", "version", pg.Version) } else { diff --git a/backend/internal/features/notifiers/models/telegram/model.go b/backend/internal/features/notifiers/models/telegram/model.go index be8a4d8..5611407 100644 --- a/backend/internal/features/notifiers/models/telegram/model.go +++ b/backend/internal/features/notifiers/models/telegram/model.go @@ -7,6 +7,7 @@ import ( "log/slog" "net/http" "net/url" + "strconv" "strings" "github.com/google/uuid" @@ -16,6 +17,7 @@ type TelegramNotifier struct { NotifierID uuid.UUID `json:"notifierId" gorm:"primaryKey;column:notifier_id"` BotToken string `json:"botToken" gorm:"not null;column:bot_token"` TargetChatID string `json:"targetChatId" gorm:"not null;column:target_chat_id"` + ThreadID *int64 `json:"threadId" gorm:"column:thread_id"` } func (t *TelegramNotifier) TableName() string { @@ -47,6 +49,10 @@ func (t *TelegramNotifier) Send(logger *slog.Logger, heading string, message str data.Set("text", fullMessage) data.Set("parse_mode", "HTML") + if t.ThreadID != nil && *t.ThreadID != 0 { + data.Set("message_thread_id", strconv.FormatInt(*t.ThreadID, 10)) + } + req, err := http.NewRequest("POST", apiURL, strings.NewReader(data.Encode())) if err != nil { return fmt.Errorf("failed to create request: %w", err) diff --git a/backend/migrations/20250809062256_add_telegram_thread_id.sql b/backend/migrations/20250809062256_add_telegram_thread_id.sql new file mode 100644 index 0000000..1085d53 --- /dev/null +++ b/backend/migrations/20250809062256_add_telegram_thread_id.sql @@ -0,0 +1,15 @@ +-- +goose Up +-- +goose StatementBegin + +ALTER TABLE telegram_notifiers + ADD COLUMN thread_id BIGINT; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin + +ALTER TABLE telegram_notifiers + DROP COLUMN IF EXISTS thread_id; + +-- +goose StatementEnd diff --git a/frontend/src/entity/notifiers/models/telegram/TelegramNotifier.ts b/frontend/src/entity/notifiers/models/telegram/TelegramNotifier.ts index 0647bb8..b2a58f0 100644 --- a/frontend/src/entity/notifiers/models/telegram/TelegramNotifier.ts +++ b/frontend/src/entity/notifiers/models/telegram/TelegramNotifier.ts @@ -1,4 +1,8 @@ export interface TelegramNotifier { botToken: string; targetChatId: string; + threadId?: number; + + // temp field + isSendToThreadEnabled?: boolean; } diff --git a/frontend/src/entity/notifiers/models/telegram/validateTelegramNotifier.ts b/frontend/src/entity/notifiers/models/telegram/validateTelegramNotifier.ts index 36fe2eb..f827bd8 100644 --- a/frontend/src/entity/notifiers/models/telegram/validateTelegramNotifier.ts +++ b/frontend/src/entity/notifiers/models/telegram/validateTelegramNotifier.ts @@ -9,5 +9,10 @@ export const validateTelegramNotifier = (notifier: TelegramNotifier): boolean => return false; } + // If thread is enabled, thread ID must be present and valid + if (notifier.isSendToThreadEnabled && (!notifier.threadId || notifier.threadId <= 0)) { + return false; + } + return true; }; diff --git a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx index 3244f8b..e36da97 100644 --- a/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx @@ -208,7 +208,7 @@ export function EditNotifierComponent({ )}
-
Type
+
Type
-
+
How to get Discord webhook URL:
diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx index a468530..f2f3387 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditEmailNotifierComponent.tsx @@ -13,7 +13,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved return ( <>
-
Target email
+
Target email
{ @@ -39,7 +39,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
-
SMTP host
+
SMTP host
{ @@ -61,7 +61,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
-
SMTP port
+
SMTP port
-
SMTP user
+
SMTP user
{ @@ -106,7 +106,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
-
SMTP password
+
SMTP password
{ diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx index 7ed2608..308a0b0 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditSlackNotifierComponent.tsx @@ -11,7 +11,7 @@ interface Props { export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) { return ( <> -
+
-
Bot token
+
Bot token
-
Target chat ID
+
Target chat ID
{ + if (notifier.telegramNotifier?.threadId && !notifier.telegramNotifier.isSendToThreadEnabled) { + setNotifier({ + ...notifier, + telegramNotifier: { + ...notifier.telegramNotifier, + isSendToThreadEnabled: true, + }, + }); + } + }, [notifier]); + return ( <>
-
Bot token
+
Bot token
-
+
-
Target chat ID
+
Target chat ID
-
+
{!isShowHowToGetChatId ? (
)}
+ +
+
Send to group topic
+ + { + if (!notifier?.telegramNotifier) return; + + setNotifier({ + ...notifier, + telegramNotifier: { + ...notifier.telegramNotifier, + isSendToThreadEnabled: checked, + // Clear thread ID if disabling + threadId: checked ? notifier.telegramNotifier.threadId : undefined, + }, + }); + setIsUnsaved(true); + }} + size="small" + /> + + + + +
+ + {notifier?.telegramNotifier?.isSendToThreadEnabled && ( + <> +
+
Thread ID
+ +
+ { + if (!notifier?.telegramNotifier) return; + + const value = e.target.value.trim(); + const threadId = value ? parseInt(value, 10) : undefined; + + setNotifier({ + ...notifier, + telegramNotifier: { + ...notifier.telegramNotifier, + threadId: !isNaN(threadId!) ? threadId : undefined, + }, + }); + setIsUnsaved(true); + }} + size="small" + className="w-full" + placeholder="3" + type="number" + min="1" + /> +
+ + + + +
+ +
+
+ To get the thread ID, go to the thread in your Telegram group, tap on the thread name + at the top, then tap “Thread Info”. Copy the thread link and take the last + number from the URL. +
+
+ Example: If the thread link is{' '} + https://t.me/c/2831948048/3, the + thread ID is 3 +
+
+ Note: Thread functionality only works in group chats, not in private + chats. +
+
+ + )} ); } diff --git a/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx b/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx index 246dc21..e79aa99 100644 --- a/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx +++ b/frontend/src/features/notifiers/ui/edit/notifiers/EditWebhookNotifierComponent.tsx @@ -14,7 +14,7 @@ export function EditWebhookNotifierComponent({ notifier, setNotifier, setIsUnsav return ( <>
-
Webhook URL
+
Webhook URL
-
Method
+
Method