mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 01:55:25 +08:00
feat(tgbot): route admin notifications to a forum topic (#6879)
Admins who run the bot in a forum supergroup got every notification in the General topic. An admin chat ID may now carry a topic, chatId:topicId, and the bot's admin messages and reports go to that topic. Repeated chats collapse into one entry and a second, different topic for the same chat is rejected, in the panel and in the docs setup helper alike. The chat ID hint in every locale and the English, Persian, Russian and Chinese guides describe the form.
This commit is contained in:
@@ -77,7 +77,7 @@ export function TelegramSetupHelper() {
|
||||
/>
|
||||
{adminIds ? (
|
||||
idsV.invalid.length > 0 ? (
|
||||
<Status ok={false}>Not numeric: {idsV.invalid.join(', ')}</Status>
|
||||
<Status ok={false}>Invalid: {idsV.invalid.join(', ')}</Status>
|
||||
) : (
|
||||
<Status ok>
|
||||
{idsV.ids.length} admin id{idsV.ids.length === 1 ? '' : 's'}
|
||||
|
||||
@@ -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.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
@@ -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 …`).|
|
||||
|
||||
@@ -37,6 +37,11 @@ icon: Send
|
||||
|
||||
در تنظیمات پنل، ربات Telegram را فعال کنید و **توکن** و **شناسه(های) چت ادمین** را
|
||||
(با کاما جداشده) تنظیم نمایید. ذخیره کنید، سپس به ربات خود پیام بدهید.
|
||||
|
||||
برای ارسال اعلانها به یک تاپیک مشخص از یک سوپرگروه فروم، شناسهی تاپیک را پس از
|
||||
دونقطه اضافه کنید، مثلاً `-1001234567890:42`. شناسهی تاپیک عدد بعد از چت در لینک یک
|
||||
پیام تاپیک است (`https://t.me/c/1234567890/42/…`). برای تاپیک General آن را ننویسید
|
||||
(یا `1` بگذارید).
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
@@ -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 …`). |
|
||||
|
||||
@@ -39,6 +39,11 @@ icon: Send
|
||||
|
||||
В настройках панели включите Telegram-бота и задайте **токен** и **админские
|
||||
chat ID** (через запятую). Сохраните, затем напишите своему боту.
|
||||
|
||||
Чтобы отправлять уведомления в определённую тему форумной супергруппы, добавьте
|
||||
ID темы после двоеточия, например `-1001234567890:42`. ID темы — число после чата
|
||||
в ссылке на сообщение темы (`https://t.me/c/1234567890/42/…`). Для темы General
|
||||
не указывайте его (или укажите `1`).
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
@@ -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 …`).|
|
||||
|
||||
@@ -36,6 +36,10 @@ icon: Send
|
||||
|
||||
在面板设置中启用 Telegram 机器人,并填写**令牌**和**管理员聊天 ID**
|
||||
(多个时以逗号分隔)。保存后,再向你的机器人发送消息。
|
||||
|
||||
要把通知发送到论坛超级群组的某个话题,在聊天 ID 后加冒号和话题 ID,例如
|
||||
`-1001234567890:42`。话题 ID 是话题消息链接中聊天之后的数字
|
||||
(`https://t.me/c/1234567890/42/…`)。General 话题不填(或填 `1`)。
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
@@ -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 …`)。 |
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string, number>();
|
||||
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 ":<topicId>" 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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -1224,7 +1224,7 @@
|
||||
"telegramAPIServer": "سرور API تلگرام",
|
||||
"telegramAPIServerDesc": "API سرور تلگرام برای اتصال را تغییر میدهد. برای استفاده از سرور پیش فرض خالی بگذارید",
|
||||
"telegramChatId": "آیدی چت مدیر",
|
||||
"telegramChatIdDesc": "دریافت کنید ('/id'یا (دستور ({'@'}userinfobot) آیدی(های) چت تلگرام مدیر، از",
|
||||
"telegramChatIdDesc": "آیدی(های) چت تلگرام مدیر، جدا شده با کاما. از {'@'}userinfobot یا دستور '/id' در ربات دریافت کنید. برای ارسال در یک تاپیک فروم، ':topicId' را اضافه کنید، مثلاً -1001234567890:42",
|
||||
"telegramNotifyTime": "زمان نوتیفیکیشن",
|
||||
"telegramNotifyTimeDesc": "هر چند وقت یکبار ربات تلگرام گزارش دورهای بفرستد. یک بازهٔ آماده انتخاب کنید یا گزینهٔ سفارشی را بزنید تا عبارت crontab وارد کنید.",
|
||||
"notifyTime": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user