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:
@@ -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