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": {