mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 07:15:01 +03: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(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user