diff --git a/docs/components/tools/telegram-setup-helper.tsx b/docs/components/tools/telegram-setup-helper.tsx index d9c9febc7..310709ad9 100644 --- a/docs/components/tools/telegram-setup-helper.tsx +++ b/docs/components/tools/telegram-setup-helper.tsx @@ -77,7 +77,7 @@ export function TelegramSetupHelper() { /> {adminIds ? ( idsV.invalid.length > 0 ? ( - Not numeric: {idsV.invalid.join(', ')} + Invalid: {idsV.invalid.join(', ')} ) : ( {idsV.ids.length} admin id{idsV.ids.length === 1 ? '' : 's'} diff --git a/docs/content/docs/en/operations/telegram-bot.mdx b/docs/content/docs/en/operations/telegram-bot.mdx index c5e4fbcf3..283adbdb5 100644 --- a/docs/content/docs/en/operations/telegram-bot.mdx +++ b/docs/content/docs/en/operations/telegram-bot.mdx @@ -37,6 +37,11 @@ connected). This is your **admin** ID. In Panel Settings, enable the Telegram bot and set the **token** and **admin chat ID(s)** (comma-separated). Save, then message your bot. + +To send notifications into a specific topic of a forum supergroup, append the +topic ID after a colon, e.g. `-1001234567890:42`. The topic ID is the number +after the chat in a topic message link (`https://t.me/c/1234567890/42/…`). Leave +the topic off (or use `1`) for the General topic. @@ -101,7 +106,7 @@ The warning lead times come from `expireDiff` (days before expiry) and | -------------- | -------------------------- | ---------------------------------------------- | | `tgBotEnable` | `false` | Master on/off. | | `tgBotToken` | _(secret)_ | Bot API token. | -| `tgBotChatId` | _(none)_ | Comma-separated **admin** Telegram IDs. | +| `tgBotChatId` | _(none)_ | Comma-separated **admin** Telegram IDs; `chatId:topicId` targets a forum topic. | | `tgBotProxy` | _(none)_ | `socks5://`, `http://`, or `https://` proxy. | | `tgBotAPIServer` | _(default)_ | Custom Telegram Bot API server. | | `tgRunTime` | `@daily` | Report schedule (cron / `@daily` / `@every …`).| diff --git a/docs/content/docs/fa/operations/telegram-bot.mdx b/docs/content/docs/fa/operations/telegram-bot.mdx index dca53156a..c900e056a 100644 --- a/docs/content/docs/fa/operations/telegram-bot.mdx +++ b/docs/content/docs/fa/operations/telegram-bot.mdx @@ -37,6 +37,11 @@ icon: Send در تنظیمات پنل، ربات Telegram را فعال کنید و **توکن** و **شناسه‌(های) چت ادمین** را (با کاما جداشده) تنظیم نمایید. ذخیره کنید، سپس به ربات خود پیام بدهید. + +برای ارسال اعلان‌ها به یک تاپیک مشخص از یک سوپرگروه فروم، شناسه‌ی تاپیک را پس از +دونقطه اضافه کنید، مثلاً `-1001234567890:42`. شناسه‌ی تاپیک عدد بعد از چت در لینک یک +پیام تاپیک است (`https://t.me/c/1234567890/42/…`). برای تاپیک General آن را ننویسید +(یا `1` بگذارید). @@ -101,7 +106,7 @@ icon: Send | -------------- | -------------------------- | ---------------------------------------------- | | `tgBotEnable` | `false` | روشن/خاموش اصلی. | | `tgBotToken` | _(محرمانه)_ | توکن API ربات. | -| `tgBotChatId` | _(هیچ‌کدام)_ | شناسه‌های Telegram **ادمین**، جداشده با کاما. | +| `tgBotChatId` | _(هیچ‌کدام)_ | شناسه‌های Telegram **ادمین**، جداشده با کاما؛ `chatId:topicId` به یک تاپیک فروم می‌فرستد. | | `tgBotProxy` | _(هیچ‌کدام)_ | پروکسی `socks5://`، `http://` یا `https://`. | | `tgBotAPIServer` | _(پیش‌فرض)_ | سرور سفارشی Telegram Bot API. | | `tgRunTime` | `@daily` | زمان‌بندی گزارش (cron / `@daily` / `@every …`). | diff --git a/docs/content/docs/ru/operations/telegram-bot.mdx b/docs/content/docs/ru/operations/telegram-bot.mdx index d9a92a74e..f321843c0 100644 --- a/docs/content/docs/ru/operations/telegram-bot.mdx +++ b/docs/content/docs/ru/operations/telegram-bot.mdx @@ -39,6 +39,11 @@ icon: Send В настройках панели включите Telegram-бота и задайте **токен** и **админские chat ID** (через запятую). Сохраните, затем напишите своему боту. + +Чтобы отправлять уведомления в определённую тему форумной супергруппы, добавьте +ID темы после двоеточия, например `-1001234567890:42`. ID темы — число после чата +в ссылке на сообщение темы (`https://t.me/c/1234567890/42/…`). Для темы General +не указывайте его (или укажите `1`). @@ -108,7 +113,7 @@ chat ID** (через запятую). Сохраните, затем напиш | -------------- | -------------------------- | ---------------------------------------------- | | `tgBotEnable` | `false` | Главный переключатель вкл./выкл. | | `tgBotToken` | _(секрет)_ | Токен Bot API. | -| `tgBotChatId` | _(нет)_ | **Админские** Telegram ID через запятую. | +| `tgBotChatId` | _(нет)_ | **Админские** Telegram ID через запятую; `chatId:topicId` — в тему форума. | | `tgBotProxy` | _(нет)_ | Прокси `socks5://`, `http://` или `https://`. | | `tgBotAPIServer` | _(по умолчанию)_ | Пользовательский сервер Telegram Bot API. | | `tgRunTime` | `@daily` | Расписание отчётов (cron / `@daily` / `@every …`).| diff --git a/docs/content/docs/zh/operations/telegram-bot.mdx b/docs/content/docs/zh/operations/telegram-bot.mdx index 6272450a6..2b9887cfd 100644 --- a/docs/content/docs/zh/operations/telegram-bot.mdx +++ b/docs/content/docs/zh/operations/telegram-bot.mdx @@ -36,6 +36,10 @@ icon: Send 在面板设置中启用 Telegram 机器人,并填写**令牌**和**管理员聊天 ID** (多个时以逗号分隔)。保存后,再向你的机器人发送消息。 + +要把通知发送到论坛超级群组的某个话题,在聊天 ID 后加冒号和话题 ID,例如 +`-1001234567890:42`。话题 ID 是话题消息链接中聊天之后的数字 +(`https://t.me/c/1234567890/42/…`)。General 话题不填(或填 `1`)。 @@ -97,7 +101,7 @@ ID 的所有客户端。订阅 ID 就是邀请码,因此请保持其足够长 | -------------- | -------------------------- | ---------------------------------------------- | | `tgBotEnable` | `false` | 总开关。 | | `tgBotToken` | _(机密)_ | 机器人 API 令牌。 | -| `tgBotChatId` | _(无)_ | 以逗号分隔的**管理员** Telegram ID。 | +| `tgBotChatId` | _(无)_ | 以逗号分隔的**管理员** Telegram ID;`chatId:topicId` 发送到论坛话题。 | | `tgBotProxy` | _(无)_ | `socks5://`、`http://` 或 `https://` 代理。 | | `tgBotAPIServer` | _(默认)_ | 自定义 Telegram Bot API 服务器。 | | `tgRunTime` | `@daily` | 报告计划(cron / `@daily` / `@every …`)。 | diff --git a/docs/lib/xray/telegram.test.ts b/docs/lib/xray/telegram.test.ts index fd2585357..36d520592 100644 --- a/docs/lib/xray/telegram.test.ts +++ b/docs/lib/xray/telegram.test.ts @@ -32,18 +32,47 @@ describe('validateBotToken', () => { describe('parseAdminIds', () => { it('splits a comma list into integer ids', () => { - expect(parseAdminIds('111, 222')).toEqual({ ids: [111, 222], invalid: [] }); + expect(parseAdminIds('111, 222')).toEqual({ + ids: [111, 222], + entries: ['111', '222'], + invalid: [], + }); }); it('accepts negative group ids and captures invalid entries', () => { expect(parseAdminIds('-1001234567, abc, 42')).toEqual({ ids: [-1001234567, 42], + entries: ['-1001234567', '42'], invalid: ['abc'], }); }); + it('drops the General topic suffix, since the Bot API rejects thread id 1', () => { + expect(parseAdminIds('-1001234567:1')).toEqual({ + ids: [-1001234567], + entries: ['-1001234567'], + invalid: [], + }); + }); + + it('rejects a second topic for the same chat, as the panel does', () => { + expect(parseAdminIds('-1001:7, -1001:1, -1001:8')).toEqual({ + ids: [-1001], + entries: ['-1001:7'], + invalid: ['-1001:8'], + }); + }); + + it('keeps a forum topic suffix and rejects a malformed one', () => { + expect(parseAdminIds('-1001234567:42, 111:0, 222:x')).toEqual({ + ids: [-1001234567], + entries: ['-1001234567:42'], + invalid: ['111:0', '222:x'], + }); + }); + it('returns empty for blank input', () => { - expect(parseAdminIds(' ')).toEqual({ ids: [], invalid: [] }); + expect(parseAdminIds(' ')).toEqual({ ids: [], entries: [], invalid: [] }); }); }); @@ -103,4 +132,13 @@ describe('buildBotConfigSummary', () => { expect(s.tgBotChatId).toBe('111,222'); expect(s.tgRunTime).toBe('@daily'); }); + + it('keeps forum topic suffixes in the chat id setting', () => { + const s = buildBotConfigSummary({ + token: VALID_TOKEN, + adminIds: '111, -1001234567 : 42', + runTime: '@daily', + }); + expect(s.tgBotChatId).toBe('111,-1001234567:42'); + }); }); diff --git a/docs/lib/xray/telegram.ts b/docs/lib/xray/telegram.ts index 6a9de9890..be0d47ed3 100644 --- a/docs/lib/xray/telegram.ts +++ b/docs/lib/xray/telegram.ts @@ -1,6 +1,6 @@ // Pure validation + templating helpers for 3x-ui's Telegram bot settings. -// Grounded in internal/web/service/tgbot/tgbot.go (admin ids parsed with -// strconv.ParseInt(_,10,64); token handed to telego.NewBot → api.telegram.org) +// Grounded in internal/web/service/tgbot/tgbot.go (admin ids parsed by +// parseAdminChats as "chatId[:topicId]"; token handed to telego.NewBot → api.telegram.org) // and the panel's tg* settings (tgRunTime uses robfig/cron). No React/DOM. export interface BotConfig { @@ -17,6 +17,7 @@ export interface TokenValidation { export interface AdminIdsResult { ids: number[]; + entries: string[]; // normalized "chatId" or "chatId:topicId", as the panel stores them invalid: string[]; } @@ -41,17 +42,29 @@ export function validateBotToken(token: string): TokenValidation { } export function parseAdminIds(raw: string): AdminIdsResult { - const ids: number[] = []; + // Mirrors parseAdminChats in tgbot.go: one topic per chat, repeats collapse. + const topics = new Map(); const invalid: string[] = []; for (const part of raw .split(',') .map((s) => s.trim()) .filter(Boolean)) { - // Telegram chat ids are integers; group/channel ids are negative. - if (/^-?\d+$/.test(part)) ids.push(Number(part)); - else invalid.push(part); + // Telegram chat ids are integers; group/channel ids are negative. An + // optional ":" targets a forum topic inside a supergroup. + const m = /^(-?\d+)\s*(?::\s*(\d+))?$/.exec(part); + if (!m || (m[2] !== undefined && Number(m[2]) <= 0)) { + invalid.push(part); + continue; + } + // General (thread 1) is reached by omitting the topic; the Bot API rejects 1. + const topic = m[2] === undefined || Number(m[2]) === 1 ? 0 : Number(m[2]); + const prev = topics.get(m[1]) ?? 0; + if (topic && prev && prev !== topic) invalid.push(part); + else topics.set(m[1], topic || prev); } - return { ids, invalid }; + const ids = [...topics.keys()].map(Number); + const entries = [...topics].map(([chat, topic]) => (topic ? `${chat}:${topic}` : chat)); + return { ids, entries, invalid }; } // robfig/cron predefined macros (note: @reboot is NOT supported). @@ -108,11 +121,11 @@ export function buildBotConfigSummary(c: BotConfig): { tgBotChatId: string; tgRunTime: string; } { - const { ids } = parseAdminIds(c.adminIds); + const { entries } = parseAdminIds(c.adminIds); return { tgBotEnable: true, tgBotToken: c.token.trim(), - tgBotChatId: ids.join(','), + tgBotChatId: entries.join(','), tgRunTime: c.runTime.trim(), }; } diff --git a/internal/web/service/tgbot/tgbot.go b/internal/web/service/tgbot/tgbot.go index 7fadde8cf..f98dfde99 100644 --- a/internal/web/service/tgbot/tgbot.go +++ b/internal/web/service/tgbot/tgbot.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "embed" + "fmt" "math/big" "net" "net/url" @@ -40,6 +41,7 @@ var ( botHandler *th.BotHandler adminIds []int64 + adminTopics map[int64]int // admin chat -> forum topic (message_thread_id) isRunning bool hostname string hashStorage *global.HashStorage @@ -321,20 +323,14 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return err } - parsedAdminIds := make([]int64, 0) - // Parse admin IDs from comma-separated string - if tgBotID != "" { - for adminID := range strings.SplitSeq(tgBotID, ",") { - id, err := strconv.ParseInt(adminID, 10, 64) - if err != nil { - logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) - return err - } - parsedAdminIds = append(parsedAdminIds, id) - } + parsedAdminIds, parsedAdminTopics, err := parseAdminChats(tgBotID) + if err != nil { + logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) + return err } tgBotMutex.Lock() adminIds = parsedAdminIds + adminTopics = parsedAdminTopics tgBotMutex.Unlock() // Get Telegram bot proxy URL @@ -500,6 +496,61 @@ func adminSnapshot() []int64 { return slices.Clone(adminIds) } +// parseAdminChats reads "chatId[:topicId],..."; the optional topic targets a +// forum topic in a supergroup, e.g. "123456789,-1001234567890:42". +func parseAdminChats(raw string) ([]int64, map[int64]int, error) { + ids := make([]int64, 0) + topics := make(map[int64]int) + if strings.TrimSpace(raw) == "" { + return ids, topics, nil + } + for entry := range strings.SplitSeq(raw, ",") { + entry = strings.TrimSpace(entry) + chatPart, topicPart, hasTopic := strings.Cut(entry, ":") + id, err := strconv.ParseInt(strings.TrimSpace(chatPart), 10, 64) + if err != nil { + return nil, nil, err + } + topic := 0 + if hasTopic { + topic, err = strconv.Atoi(strings.TrimSpace(topicPart)) + if err != nil { + return nil, nil, err + } + if topic <= 0 { + return nil, nil, fmt.Errorf("invalid topic ID %q for chat %d", topicPart, id) + } + } + // General is thread 1 internally, but the Bot API only reaches it with no thread id. + if hasTopic && topic != generalTopicID { + if prev, ok := topics[id]; ok && prev != topic { + return nil, nil, fmt.Errorf("chat %d has more than one topic configured", id) + } + topics[id] = topic + } + if !slices.Contains(ids, id) { + ids = append(ids, id) + } + } + return ids, topics, nil +} + +const generalTopicID = 1 + +// adminTopic returns the forum topic configured for chatId, or 0 when the +// chat has none and messages should go to the chat itself. +func adminTopic(chatId int64) int { + tgBotMutex.Lock() + defer tgBotMutex.Unlock() + return adminTopics[chatId] +} + +// inAdminTopic routes a document to the configured forum topic of its chat. +func inAdminTopic(document *telego.SendDocumentParams) *telego.SendDocumentParams { + document.MessageThreadID = adminTopic(document.ChatID.ID) + return document +} + // SetHostname sets the hostname for the bot. func (t *Tgbot) SetHostname() { host, err := os.Hostname() @@ -518,6 +569,7 @@ func (t *Tgbot) Stop() { logger.Info("Stop Telegram receiver ...") tgBotMutex.Lock() adminIds = nil + adminTopics = nil tgBotMutex.Unlock() } diff --git a/internal/web/service/tgbot/tgbot_client.go b/internal/web/service/tgbot/tgbot_client.go index 952482d30..275b645ae 100644 --- a/internal/web/service/tgbot/tgbot_client.go +++ b/internal/web/service/tgbot/tgbot_client.go @@ -320,7 +320,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) { tu.ID(chatId), tu.FileFromBytes(png, "sub.png"), ) - _, _ = bot.SendDocument(context.Background(), document) + _, _ = bot.SendDocument(context.Background(), inAdminTopic(document)) } else { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) } @@ -332,7 +332,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) { tu.ID(chatId), tu.FileFromBytes(png, "subjson.png"), ) - _, _ = bot.SendDocument(context.Background(), document) + _, _ = bot.SendDocument(context.Background(), inAdminTopic(document)) } else { t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error()) } @@ -349,7 +349,7 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) { tu.ID(chatId), tu.FileFromBytes(png, filename), ) - _, _ = bot.SendDocument(context.Background(), document) + _, _ = bot.SendDocument(context.Background(), inAdminTopic(document)) // Reduced delay for better performance if i < max-1 { // Only delay between documents, not after the last one time.Sleep(50 * time.Millisecond) diff --git a/internal/web/service/tgbot/tgbot_report.go b/internal/web/service/tgbot/tgbot_report.go index 502b45a35..d48bc359d 100644 --- a/internal/web/service/tgbot/tgbot_report.go +++ b/internal/web/service/tgbot/tgbot_report.go @@ -432,7 +432,7 @@ func (t *Tgbot) sendBackupData(chatId int64, dbData []byte, dbFilename string) { tu.ID(chatId), tu.FileFromBytes(dbData, dbFilename), ) - _, err := bot.SendDocument(ctx, document) + _, err := bot.SendDocument(ctx, inAdminTopic(document)) cancel() if err != nil { logger.Error("Error in uploading backup: ", err) @@ -452,7 +452,7 @@ func (t *Tgbot) sendBackupData(chatId int64, dbData []byte, dbFilename string) { tu.ID(chatId), tu.File(file), ) - _, err = bot.SendDocument(ctx, document) + _, err = bot.SendDocument(ctx, inAdminTopic(document)) if err != nil { logger.Error("Error in uploading config.json: ", err) } @@ -478,7 +478,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { tu.ID(chatId), tu.File(file), ) - _, err = bot.SendDocument(context.Background(), document) + _, err = bot.SendDocument(context.Background(), inAdminTopic(document)) if err != nil { logger.Error("Error in uploading IPLimitBannedPrevLog: ", err) } @@ -499,7 +499,7 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) { tu.ID(chatId), tu.File(file), ) - _, err = bot.SendDocument(context.Background(), document) + _, err = bot.SendDocument(context.Background(), inAdminTopic(document)) if err != nil { logger.Error("Error in uploading IPLimitBannedLog: ", err) } diff --git a/internal/web/service/tgbot/tgbot_send.go b/internal/web/service/tgbot/tgbot_send.go index 6765bbc0f..c3f8211f4 100644 --- a/internal/web/service/tgbot/tgbot_send.go +++ b/internal/web/service/tgbot/tgbot_send.go @@ -133,9 +133,10 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R allMessages := pageMessage(msg, telegramPageLimit) for n, message := range allMessages { params := telego.SendMessageParams{ - ChatID: tu.ID(chatId), - Text: message, - ParseMode: "HTML", + ChatID: tu.ID(chatId), + MessageThreadID: adminTopic(chatId), + Text: message, + ParseMode: "HTML", } // only add replyMarkup to last message if len(replyMarkup) > 0 && n == (len(allMessages)-1) { @@ -260,9 +261,10 @@ func (t *Tgbot) SendMsgToTgbotDeleteAfter(chatId int64, msg string, delayInSecon // Send the message sentMsg, err := bot.SendMessage(context.Background(), &telego.SendMessageParams{ - ChatID: tu.ID(chatId), - Text: msg, - ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value + ChatID: tu.ID(chatId), + MessageThreadID: adminTopic(chatId), + Text: msg, + ReplyMarkup: replyMarkupParam, // Use the correct replyMarkup value }) if err != nil { logger.Warning("Failed to send message:", err) diff --git a/internal/web/service/tgbot/tgbot_test.go b/internal/web/service/tgbot/tgbot_test.go index f770cb321..acd34e274 100644 --- a/internal/web/service/tgbot/tgbot_test.go +++ b/internal/web/service/tgbot/tgbot_test.go @@ -4,8 +4,12 @@ import ( "io" "net" "reflect" + "slices" "testing" "time" + + "github.com/mymmrac/telego" + tu "github.com/mymmrac/telego/telegoutil" ) func TestLoginAttemptDoesNotCarryPassword(t *testing.T) { @@ -154,3 +158,67 @@ func TestIsCommandForBotKeepsLegacyBehaviorWhenUsernameUnavailable(t *testing.T) t.Fatal("commands must remain accepted when the current bot username is unavailable") } } + +func TestParseAdminChats(t *testing.T) { + cases := []struct { + name string + raw string + ids []int64 + topics map[int64]int + }{ + {name: "empty", raw: "", ids: []int64{}, topics: map[int64]int{}}, + {name: "plain ids", raw: "123,-1001", ids: []int64{123, -1001}, topics: map[int64]int{}}, + {name: "topic", raw: "-1003855200872:42", ids: []int64{-1003855200872}, topics: map[int64]int{-1003855200872: 42}}, + {name: "mixed with spaces", raw: " 123 , -1001 : 7 ", ids: []int64{123, -1001}, topics: map[int64]int{-1001: 7}}, + {name: "general topic means no thread", raw: "-1001:1,123", ids: []int64{-1001, 123}, topics: map[int64]int{}}, + {name: "duplicate chat collapses", raw: "-1001:7,-1001:7,-1001", ids: []int64{-1001}, topics: map[int64]int{-1001: 7}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + ids, topics, err := parseAdminChats(tc.raw) + if err != nil { + t.Fatalf("parseAdminChats(%q) error: %v", tc.raw, err) + } + if !slices.Equal(ids, tc.ids) { + t.Fatalf("ids = %v, want %v", ids, tc.ids) + } + if len(topics) != len(tc.topics) { + t.Fatalf("topics = %v, want %v", topics, tc.topics) + } + for chat, topic := range tc.topics { + if topics[chat] != topic { + t.Fatalf("topics[%d] = %d, want %d", chat, topics[chat], topic) + } + } + }) + } +} + +func TestParseAdminChatsRejectsInvalid(t *testing.T) { + for _, raw := range []string{"abc", "123:", "123:abc", "123:0", "123:-5", "-1001:7,-1001:8", "123,"} { + if _, _, err := parseAdminChats(raw); err == nil { + t.Errorf("parseAdminChats(%q) accepted invalid input", raw) + } + } +} + +func TestInAdminTopicRoutesConfiguredChat(t *testing.T) { + tgBotMutex.Lock() + orig := adminTopics + adminTopics = map[int64]int{-1001: 42} + tgBotMutex.Unlock() + t.Cleanup(func() { + tgBotMutex.Lock() + adminTopics = orig + tgBotMutex.Unlock() + }) + + doc := inAdminTopic(tu.Document(tu.ID(-1001), tu.FileFromBytes([]byte("x"), "x.db"))) + if doc.MessageThreadID != 42 { + t.Fatalf("MessageThreadID = %d, want 42", doc.MessageThreadID) + } + doc = inAdminTopic(&telego.SendDocumentParams{ChatID: tu.ID(123)}) + if doc.MessageThreadID != 0 { + t.Fatalf("MessageThreadID = %d for chat without topic, want 0", doc.MessageThreadID) + } +} diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index e02e18729..0ff67226d 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "خادم API لتيليجرام", "telegramAPIServerDesc": "سيرفر Telegram API المستخدم. سيبه فاضي لاستخدام الافتراضي.", "telegramChatId": "ID شات الأدمن", - "telegramChatIdDesc": "ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من {'@'}userinfobot) أو (استخدم '/id' في البوت)", + "telegramChatIdDesc": "ID شات الأدمن في Telegram. (مفصول بفواصل)(تقدر تجيبه من {'@'}userinfobot) أو (استخدم '/id' في البوت) للنشر في موضوع منتدى، أضف ':topicId'، مثل -1001234567890:42", "telegramNotifyTime": "وقت الإشعار", "telegramNotifyTimeDesc": "عدد مرات إرسال البوت للتقارير الدورية. اختر فترة جاهزة، أو اختر «مخصص» لإدخال تعبير crontab.", "notifyTime": { diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 10b336f74..d923a9971 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -1343,7 +1343,7 @@ "telegramAPIServer": "Telegram API Server", "telegramAPIServerDesc": "The Telegram API server to use. Leave blank to use the default server.", "telegramChatId": "Admin Chat ID", - "telegramChatIdDesc": "The Telegram Admin Chat ID(s). (comma-separated)(get it here {'@'}userinfobot) or (use '/id' command in the bot)", + "telegramChatIdDesc": "The Telegram Admin Chat ID(s). (comma-separated)(get it here {'@'}userinfobot) or (use '/id' command in the bot). To post into a forum topic, append ':topicId', e.g. -1001234567890:42", "telegramNotifyTime": "Notification Time", "telegramNotifyTimeDesc": "How often the Telegram bot sends periodic reports. Pick a preset interval, or choose Custom to enter a raw crontab expression.", "notifyTime": { diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index d534624f2..8ab845802 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Servidor API de Telegram", "telegramAPIServerDesc": "El servidor API de Telegram a utilizar. Déjelo en blanco para utilizar el servidor predeterminado.", "telegramChatId": "IDs de Chat de Telegram para Administradores", - "telegramChatIdDesc": "IDs de Chat múltiples separados por comas. Use {'@'}userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat.", + "telegramChatIdDesc": "IDs de Chat múltiples separados por comas. Use {'@'}userinfobot o use el comando '/id' en el bot para obtener sus IDs de Chat. Para publicar en un tema de foro, añada ':topicId', p. ej. -1001234567890:42", "telegramNotifyTime": "Hora de Notificación del Bot de Telegram", "telegramNotifyTimeDesc": "Con qué frecuencia el bot de Telegram envía informes periódicos. Elige un intervalo predefinido o selecciona Personalizado para introducir una expresión crontab.", "notifyTime": { diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index a6ade6731..aee1ab995 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -1224,7 +1224,7 @@ "telegramAPIServer": "سرور API تلگرام", "telegramAPIServerDesc": "API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید", "telegramChatId": "آی‌دی چت مدیر", - "telegramChatIdDesc": "دریافت ‌کنید ('/id'یا (دستور ({'@'}userinfobot) آی‌دی(های) چت تلگرام مدیر، از", + "telegramChatIdDesc": "آی‌دی(های) چت تلگرام مدیر، جدا شده با کاما. از {'@'}userinfobot یا دستور '/id' در ربات دریافت کنید. برای ارسال در یک تاپیک فروم، ':topicId' را اضافه کنید، مثلاً -1001234567890:42", "telegramNotifyTime": "زمان نوتیفیکیشن", "telegramNotifyTimeDesc": "هر چند وقت یک‌بار ربات تلگرام گزارش دوره‌ای بفرستد. یک بازهٔ آماده انتخاب کنید یا گزینهٔ سفارشی را بزنید تا عبارت crontab وارد کنید.", "notifyTime": { diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index bf45d55e2..d2e5c05dc 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Server API Telegram", "telegramAPIServerDesc": "Server API Telegram yang akan digunakan. Biarkan kosong untuk menggunakan server default.", "telegramChatId": "ID Obrolan Admin", - "telegramChatIdDesc": "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini {'@'}userinfobot) atau (gunakan perintah '/id' di bot)", + "telegramChatIdDesc": "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini {'@'}userinfobot) atau (gunakan perintah '/id' di bot). Untuk mengirim ke topik forum, tambahkan ':topicId', mis. -1001234567890:42", "telegramNotifyTime": "Waktu Notifikasi", "telegramNotifyTimeDesc": "Seberapa sering bot Telegram mengirim laporan berkala. Pilih interval siap pakai, atau pilih Kustom untuk memasukkan ekspresi crontab.", "notifyTime": { diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 2e290f3d3..63fcf40a5 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Telegram API サーバー", "telegramAPIServerDesc": "使用するTelegram APIサーバー。空白の場合はデフォルトサーバーを使用する", "telegramChatId": "管理者チャットID", - "telegramChatIdDesc": "Telegram管理者チャットID(複数の場合はカンマで区切る){'@'}userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する", + "telegramChatIdDesc": "Telegram管理者チャットID(複数の場合はカンマで区切る){'@'}userinfobotで取得するか、ボットで'/id'コマンドを使用して取得する フォーラムのトピックに投稿するには ':topicId' を付けます(例: -1001234567890:42)", "telegramNotifyTime": "通知時間", "telegramNotifyTimeDesc": "Telegram ボットが定期レポートを送信する頻度です。プリセットの間隔を選ぶか、「カスタム」を選んで crontab 式を入力します。", "notifyTime": { diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index bf5b53f06..80bb914ec 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Servidor API do Telegram", "telegramAPIServerDesc": "O servidor API do Telegram a ser usado. Deixe em branco para usar o servidor padrão.", "telegramChatId": "ID de Chat do Administrador", - "telegramChatIdDesc": "O(s) ID(s) de Chat do Administrador no Telegram. (separado por vírgulas)(obtenha aqui {'@'}userinfobot) ou (use o comando '/id' no bot)", + "telegramChatIdDesc": "O(s) ID(s) de Chat do Administrador no Telegram. (separado por vírgulas)(obtenha aqui {'@'}userinfobot) ou (use o comando '/id' no bot). Para publicar em um tópico de fórum, acrescente ':topicId', ex.: -1001234567890:42", "telegramNotifyTime": "Hora da Notificação", "telegramNotifyTimeDesc": "Com que frequência o bot do Telegram envia relatórios periódicos. Escolha um intervalo predefinido ou selecione Personalizado para inserir uma expressão crontab.", "notifyTime": { diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 4ace5f356..5aff683aa 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Telegram API Server", "telegramAPIServerDesc": "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию.", "telegramChatId": "User ID администратора бота", - "telegramChatIdDesc": "Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте {'@'}userinfobot или команду '/id' в боте.", + "telegramChatIdDesc": "Один или несколько User ID администратора(-ов) Telegram-бота. Для получения User ID используйте {'@'}userinfobot или команду '/id' в боте. Чтобы отправлять в тему форума, добавьте ':topicId', например -1001234567890:42", "telegramNotifyTime": "Частота уведомлений для администраторов от бота", "telegramNotifyTimeDesc": "Как часто бот Telegram отправляет периодические отчёты. Выберите готовый интервал или «Произвольный», чтобы ввести выражение crontab.", "notifyTime": { diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index e429ffaac..174a22aae 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Telegram API Sunucusu", "telegramAPIServerDesc": "Kullanılacak Telegram API sunucusu. Varsayılan sunucuyu kullanmak için boş bırakın.", "telegramChatId": "Yönetici Sohbet Kimliği", - "telegramChatIdDesc": "Telegram Yönetici Sohbet Kimliği (Chat ID). Birden fazla ise virgülle ayırın. ({'@'}userinfobot'tan alabilirsiniz veya botta '/id' komutunu kullanabilirsiniz.)", + "telegramChatIdDesc": "Telegram Yönetici Sohbet Kimliği (Chat ID). Birden fazla ise virgülle ayırın. ({'@'}userinfobot'tan alabilirsiniz veya botta '/id' komutunu kullanabilirsiniz.) Bir forum konusuna göndermek için ':topicId' ekleyin, ör. -1001234567890:42", "telegramNotifyTime": "Bildirim Zamanı", "telegramNotifyTimeDesc": "Telegram botunun periyodik raporları gönderme sıklığı. Hazır bir aralık seçin veya bir crontab ifadesi girmek için Özel'i seçin.", "notifyTime": { diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 0f23326cc..9cb845c09 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Telegram API сервер", "telegramAPIServerDesc": "Сервер Telegram API для використання. Залиште поле порожнім, щоб використовувати сервер за умовчанням.", "telegramChatId": "Ідентифікатор чату адміністратора", - "telegramChatIdDesc": "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут {'@'}userinfobot) або (використовуйте команду '/id' у боті)", + "telegramChatIdDesc": "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут {'@'}userinfobot) або (використовуйте команду '/id' у боті) Щоб надсилати в тему форуму, додайте ':topicId', наприклад -1001234567890:42", "telegramNotifyTime": "Час сповіщення", "telegramNotifyTimeDesc": "Як часто бот Telegram надсилає періодичні звіти. Виберіть готовий інтервал або «Власний», щоб ввести вираз crontab.", "notifyTime": { diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index ba659183d..0b1e5d5f2 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Máy chủ API Telegram", "telegramAPIServerDesc": "Máy chủ API Telegram để sử dụng. Để trống để sử dụng máy chủ mặc định.", "telegramChatId": "Chat ID Telegram của quản trị viên", - "telegramChatIdDesc": "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng {'@'}userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn.", + "telegramChatIdDesc": "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng {'@'}userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn. Để gửi vào một chủ đề diễn đàn, thêm ':topicId', ví dụ -1001234567890:42", "telegramNotifyTime": "Thời gian thông báo của bot Telegram", "telegramNotifyTimeDesc": "Tần suất bot Telegram gửi báo cáo định kỳ. Chọn một khoảng thời gian có sẵn, hoặc chọn Tùy chỉnh để nhập biểu thức crontab.", "notifyTime": { diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 9b34ce068..66251a31e 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Telegram API 服务器", "telegramAPIServerDesc": "要使用的 Telegram API 服务器。留空以使用默认服务器。", "telegramChatId": "管理员聊天 ID", - "telegramChatIdDesc": "Telegram 管理员聊天 ID (多个以逗号分隔)(可通过 {'@'}userinfobot 获取,或在机器人中使用 '/id' 命令获取)", + "telegramChatIdDesc": "Telegram 管理员聊天 ID (多个以逗号分隔)(可通过 {'@'}userinfobot 获取,或在机器人中使用 '/id' 命令获取) 如需发送到论坛话题,请追加 ':topicId',例如 -1001234567890:42", "telegramNotifyTime": "通知时间", "telegramNotifyTimeDesc": "Telegram 机器人发送周期性报告的频率。选择预设间隔,或选择“自定义”以输入 crontab 表达式。", "notifyTime": { diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index e0cfb4ca3..0881a3e1b 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -1220,7 +1220,7 @@ "telegramAPIServer": "Telegram API 伺服器", "telegramAPIServerDesc": "要使用的 Telegram API 伺服器。留空以使用預設伺服器。", "telegramChatId": "管理員聊天 ID", - "telegramChatIdDesc": "Telegram 管理員聊天 ID (多個以逗號分隔)(可通過 {'@'}userinfobot 獲取,或在機器人中使用 '/id' 命令獲取)", + "telegramChatIdDesc": "Telegram 管理員聊天 ID (多個以逗號分隔)(可通過 {'@'}userinfobot 獲取,或在機器人中使用 '/id' 命令獲取) 如需發送到論壇話題,請追加 ':topicId',例如 -1001234567890:42", "telegramNotifyTime": "通知時間", "telegramNotifyTimeDesc": "Telegram 機器人傳送週期性報告的頻率。選擇預設間隔,或選擇「自訂」以輸入 crontab 運算式。", "notifyTime": {