mirror of
https://github.com/Trollobot/Telemax.git
synced 2026-10-19 10:23:24 +00:00
1.3.2-beta.3: всё, что пульт просит выбрать или нажать, появляется в самом пульте — списки бана/разбана, проверка обновления с «Обновить», «Команды», ссылка «📤 Загрузить файл» и её итог; у результата остаётся «◀️ Назад»
This commit is contained in:
parent
5dc919da9d
commit
9ff1c3e7a8
7 changed files with 123 additions and 63 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "telemax",
|
||||
"version": "1.3.2-beta.2",
|
||||
"version": "1.3.2-beta.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "telemax",
|
||||
"version": "1.3.2-beta.2",
|
||||
"version": "1.3.2-beta.3",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "telemax",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"version": "1.3.2-beta.2",
|
||||
"version": "1.3.2-beta.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/server/app.ts",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { formatBytes } from './status.js';
|
|||
import { uploadFileFromDiskToMax } from './upload.js';
|
||||
import { isTransientHttpStatus, isTransientNetworkError, TransientDownloadError } from './transient.js';
|
||||
import { reportError } from './telemetry.js';
|
||||
import { keepPanelBack } from './panelBack.js';
|
||||
|
||||
/**
|
||||
* What the topic is told about a failure: MAX's own answers are readable Russian and stay as they
|
||||
|
|
@ -62,8 +63,8 @@ export interface BigFiles {
|
|||
offerUpload(p: { maxChatId: string; topicId: number; telegramMessageId: number; caption?: string; name?: string; size?: number }): Promise<void>;
|
||||
/** MAX → Telegram: a FILE over the bot upload limit — save it and post a download link. */
|
||||
relayFromMax(att: MaxAttachment, ctx: DownloadContext, opts: SendOpts): Promise<SentMessage>;
|
||||
/** The panel's «📤 Загрузить файл»: an upload link into the server's own store, posted in General. */
|
||||
offerStore(): Promise<void>;
|
||||
/** The panel's «📤 Загрузить файл»: an upload link into the server's own store, drawn in the panel message. */
|
||||
offerStore(panelMessageId: number): Promise<void>;
|
||||
/** The panel's «📤 Отправить в MAX»: a stored file that didn't make it, sent again from disk. */
|
||||
resendToMax(id: string): Promise<{ ok: boolean; text: string }>;
|
||||
}
|
||||
|
|
@ -117,8 +118,10 @@ export function createBigFiles({ bot, max, targetGroupId, fileShare, onSentToMax
|
|||
// --- Telegram → MAX -------------------------------------------------------------------------
|
||||
|
||||
const CANCEL = 'tlmx_panel:fcancel:';
|
||||
const uploadKeyboard = (url: string, token: string) =>
|
||||
Markup.inlineKeyboard([[Markup.button.url('📤 Загрузить файл', url)], [Markup.button.callback('✖️ Отменить', CANCEL + token)]]);
|
||||
const uploadRows = (url: string, token: string) => [[Markup.button.url('📤 Загрузить файл', url)], [Markup.button.callback('✖️ Отменить', CANCEL + token)]];
|
||||
const uploadKeyboard = (url: string, token: string) => Markup.inlineKeyboard(uploadRows(url, token));
|
||||
/** The way back to «📁 Файлы» for everything the file exchange draws in the panel message. */
|
||||
const TO_FILES = [Markup.button.callback('◀️ К файлам', 'tlmx_panel:files')];
|
||||
|
||||
async function offerUpload(p: Parameters<BigFiles['offerUpload']>[0]): Promise<void> {
|
||||
const fromTelegram = p.name != null && p.size != null;
|
||||
|
|
@ -186,43 +189,48 @@ export function createBigFiles({ bot, max, targetGroupId, fileShare, onSentToMax
|
|||
if (ticket) logger.info(`Upload link for «${ticket.name || 'any file'}» cancelled`);
|
||||
if (ticket) await fileShare.dropTicket(ticket.token);
|
||||
const text = (ctx.callbackQuery.message as { text?: string } | undefined)?.text;
|
||||
await ctx.editMessageText(ticket || !text ? '✖️ Загрузка отменена — ссылка больше не работает.' : text).catch(() => {});
|
||||
// In the panel (the file exchange) the cancelled link keeps «◀️ К файлам».
|
||||
await ctx.editMessageText(ticket || !text ? '✖️ Загрузка отменена — ссылка больше не работает.' : text, keepPanelBack(ctx)).catch(() => {});
|
||||
});
|
||||
|
||||
async function offerStore(): Promise<void> {
|
||||
/**
|
||||
* The panel's «📤 Загрузить файл», drawn IN the panel message: the admin is looking at the panel,
|
||||
* and a link posted at the bottom of the chat went unseen (2026-10-11). The finished upload lands
|
||||
* in the same message, with «◀️ К файлам».
|
||||
*/
|
||||
async function offerStore(panelId: number): Promise<void> {
|
||||
const show = (text: string, rows: ReturnType<typeof uploadRows> = []) => edit(panelId, 0, text, Markup.inlineKeyboard([...rows, TO_FILES]));
|
||||
if (!fileShare.enabled) {
|
||||
await send('⚠️ Пересылка больших файлов отключена в настройках (FILES=off).', {});
|
||||
await show('⚠️ Пересылка больших файлов отключена в настройках (FILES=off).');
|
||||
return;
|
||||
}
|
||||
const room = await fileShare.roomFor(1);
|
||||
if (!room.fits) {
|
||||
await send(`⚠️ Загрузить файл не выйдет: на сервере свободно ${formatBytes(room.freeBytes)}, а ${reserveText} нужно оставить для обновлений.`, {});
|
||||
await show(`⚠️ Загрузить файл не выйдет: на сервере свободно ${formatBytes(room.freeBytes)}, а ${reserveText} нужно оставить для обновлений.`);
|
||||
return;
|
||||
}
|
||||
const prompt = await send('⏳ Готовлю ссылку для загрузки файла на сервер…', {});
|
||||
await show('⏳ Готовлю ссылку для загрузки файла на сервер…');
|
||||
const ticket = await fileShare.createUploadTicket({
|
||||
purpose: 'store',
|
||||
maxChatId: '',
|
||||
topicId: 0,
|
||||
telegramMessageId: prompt.message_id,
|
||||
telegramMessageId: panelId,
|
||||
name: '',
|
||||
expectedSize: 0,
|
||||
promptMessageId: prompt.message_id,
|
||||
promptMessageId: panelId,
|
||||
});
|
||||
logger.info('Panel: offering an upload link into the server store');
|
||||
const svc = await fileShare.ensureService();
|
||||
if (!svc.ok) {
|
||||
report('service', svc.reason);
|
||||
await fileShare.dropTicket(ticket.token);
|
||||
await edit(prompt.message_id, 0, `⚠️ Ссылку для загрузки подготовить не удалось — ${svc.reason}.`);
|
||||
await show(`⚠️ Ссылку для загрузки подготовить не удалось — ${svc.reason}.`);
|
||||
return;
|
||||
}
|
||||
await edit(
|
||||
prompt.message_id,
|
||||
0,
|
||||
`📤 Загрузите файл по ссылке — он сохранится на вашем сервере, а ссылку для скачивания я пришлю сюда же. Ею можно делиться где угодно.
|
||||
await show(
|
||||
`📤 Загрузите файл по ссылке — он сохранится на вашем сервере, а ссылка для скачивания появится здесь же. Ею можно делиться где угодно.
|
||||
Ссылка на загрузку действует до ${formatExpiry(ticket.expiresAt)}.`,
|
||||
uploadKeyboard(`${svc.url}/f/${ticket.token}`, ticket.token),
|
||||
uploadRows(`${svc.url}/f/${ticket.token}`, ticket.token),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -234,7 +242,7 @@ export function createBigFiles({ bot, max, targetGroupId, fileShare, onSentToMax
|
|||
const svc = await fileShare.ensureService(30_000);
|
||||
const until = formatExpiry(file.expiresAt);
|
||||
if (!svc.ok) {
|
||||
await edit(ticket.promptMessageId, 0, `✅ Файл ${label} на сервере до ${until}. Ссылку можно взять в пульте → 📁 Файлы.`);
|
||||
await edit(ticket.promptMessageId, 0, `✅ Файл ${label} на сервере до ${until}. Ссылку можно взять в пульте → 📁 Файлы.`, Markup.inlineKeyboard([TO_FILES]));
|
||||
return;
|
||||
}
|
||||
await edit(
|
||||
|
|
@ -243,7 +251,7 @@ export function createBigFiles({ bot, max, targetGroupId, fileShare, onSentToMax
|
|||
`✅ Файл ${label} на сервере до ${until}.
|
||||
🔗 ${svc.url}/f/${file.token}
|
||||
Ссылкой можно делиться — по ней откроется страница со скачиванием. Продлить или удалить — в пульте → 📁 Файлы.`,
|
||||
Markup.inlineKeyboard([Markup.button.url('⬇️ Скачать', `${svc.url}/f/${file.token}`)]),
|
||||
Markup.inlineKeyboard([[Markup.button.url('⬇️ Скачать', `${svc.url}/f/${file.token}`)], TO_FILES]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import path from 'node:path';
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
||||
import { Markup, type Telegraf, type Context } from 'telegraf';
|
||||
import type { InlineKeyboardButton } from 'telegraf/types';
|
||||
import type { MaxClient, MaxContactInfo } from '../max/client.js';
|
||||
import { getAppVersion } from './version.js';
|
||||
import { formatBytes, maskPhone } from './status.js';
|
||||
|
|
@ -11,8 +12,9 @@ import type { FileShare, StoredFile } from './fileShare.js';
|
|||
|
||||
const logger = createLogger('panel');
|
||||
|
||||
/** Sends one of the reused command outputs (help, version, ban/unban list) to a chat. */
|
||||
type LeafSender = (chatId: number) => Promise<void>;
|
||||
/** One of the reused command outputs (help, version, ban/unban list): text plus its own button rows. The panel draws it in place and adds «◀️ Назад». */
|
||||
export type LeafView = { text: string; rows: InlineKeyboardButton[][] };
|
||||
type LeafBuilder = () => Promise<LeafView>;
|
||||
|
||||
export interface ControlPanelDeps {
|
||||
bot: Telegraf;
|
||||
|
|
@ -20,12 +22,12 @@ export interface ControlPanelDeps {
|
|||
max: MaxClient;
|
||||
getActivePhone: () => string;
|
||||
triggerFullResync: () => Promise<void>;
|
||||
/** Reused slash-command bodies — the panel just triggers the same output. */
|
||||
/** Reused slash-command bodies — the panel shows the same output in its own message. */
|
||||
leaves: {
|
||||
sendHelp: LeafSender;
|
||||
sendVersion: LeafSender;
|
||||
sendBanList: LeafSender;
|
||||
sendUnbanList: LeafSender;
|
||||
help: LeafBuilder;
|
||||
version: LeafBuilder;
|
||||
banList: LeafBuilder;
|
||||
unbanList: LeafBuilder;
|
||||
};
|
||||
/** Reuses or creates a MAX dialog with the contact and its Telegram topic; returns a deep link to open it. */
|
||||
startDialog: (
|
||||
|
|
@ -42,8 +44,8 @@ export interface ControlPanelDeps {
|
|||
pause: PauseControl;
|
||||
/** Big files kept for download links — the «📁 Файлы» section. */
|
||||
files?: FileShare;
|
||||
/** «📤 Загрузить файл»: an upload link into the server store, posted in General (bigFiles.ts). */
|
||||
offerStore?: () => Promise<void>;
|
||||
/** «📤 Загрузить файл»: an upload link into the server store, drawn in the panel message (bigFiles.ts). */
|
||||
offerStore?: (panelMessageId: number) => Promise<void>;
|
||||
/** «📤 Отправить в MAX» for a stored file that didn't make it (bigFiles.ts). */
|
||||
resendToMax?: (id: string) => Promise<{ ok: boolean; text: string }>;
|
||||
}
|
||||
|
|
@ -433,7 +435,8 @@ export function wireControlPanel(deps: ControlPanelDeps): void {
|
|||
});
|
||||
bot.action('tlmx_panel:fupload', async (ctx) => {
|
||||
await ctx.answerCbQuery('Готовлю ссылку для загрузки…').catch(() => {});
|
||||
if (offerStore) void offerStore().catch((err) => logger.error('Panel store upload failed', err));
|
||||
const panelId = ctx.callbackQuery.message?.message_id;
|
||||
if (offerStore && panelId != null) void offerStore(panelId).catch((err) => logger.error('Panel store upload failed', err));
|
||||
});
|
||||
bot.action(/^tlmx_panel:fdel:(.+)$/, async (ctx) => {
|
||||
const name = files?.get(ctx.match[1] ?? '')?.name;
|
||||
|
|
@ -448,22 +451,35 @@ export function wireControlPanel(deps: ControlPanelDeps): void {
|
|||
await ctx.deleteMessage().catch(() => {});
|
||||
});
|
||||
|
||||
// --- Leaves that spawn their own message (reuse the slash-command output) -----------
|
||||
// --- Leaves: the slash-command output, drawn IN the panel ---------------------------
|
||||
// The admin is looking at the panel (often scrolled up to the pin): a list or a button posted at
|
||||
// the bottom of the chat went unseen (2026-10-11). «◀️ Назад» returns to the menu, and a press
|
||||
// inside the leaf keeps it (keepPanelBack in panelBack.ts).
|
||||
async function showLeaf(ctx: Context, build: LeafBuilder, back: string): Promise<void> {
|
||||
let view: LeafView;
|
||||
try {
|
||||
view = await build();
|
||||
} catch (err) {
|
||||
logger.error('Panel leaf failed', err);
|
||||
view = { text: '❌ Не получилось — смотрите логи контейнера.', rows: [] };
|
||||
}
|
||||
await edit(ctx, { text: view.text, markup: Markup.inlineKeyboard([...view.rows, [Markup.button.callback('◀️ Назад', back)]]).reply_markup });
|
||||
}
|
||||
bot.action('tlmx_panel:help', async (ctx) => {
|
||||
await ctx.answerCbQuery();
|
||||
await leaves.sendHelp(chatIdOf(ctx));
|
||||
await showLeaf(ctx, leaves.help, 'tlmx_panel:system');
|
||||
});
|
||||
bot.action('tlmx_panel:update', async (ctx) => {
|
||||
await ctx.answerCbQuery();
|
||||
await leaves.sendVersion(chatIdOf(ctx));
|
||||
await ctx.answerCbQuery('Проверяю обновления…');
|
||||
await showLeaf(ctx, leaves.version, 'tlmx_panel:system');
|
||||
});
|
||||
bot.action('tlmx_panel:ban', async (ctx) => {
|
||||
await ctx.answerCbQuery();
|
||||
await leaves.sendBanList(chatIdOf(ctx));
|
||||
await showLeaf(ctx, leaves.banList, 'tlmx_panel:chats');
|
||||
});
|
||||
bot.action('tlmx_panel:unban', async (ctx) => {
|
||||
await ctx.answerCbQuery();
|
||||
await leaves.sendUnbanList(chatIdOf(ctx));
|
||||
await showLeaf(ctx, leaves.unbanList, 'tlmx_panel:chats');
|
||||
});
|
||||
bot.action('tlmx_panel:resync', async (ctx) => {
|
||||
// Without a MAX session CHATS_LIST is rejected ("Недопустимое состояние сессии") and the only
|
||||
|
|
|
|||
16
src/bridge/panelBack.ts
Normal file
16
src/bridge/panelBack.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Markup, type Context } from 'telegraf';
|
||||
import type { InlineKeyboardButton, InlineKeyboardMarkup } from 'telegraf/types';
|
||||
|
||||
/**
|
||||
* The panel's own «◀️ …» button on the message a button was pressed in, kept for the result. The panel
|
||||
* draws lists and prompts (ban/unban, version, the upload link) IN its own message, so a press that
|
||||
* rewrites that message must leave a way back — or the pinned panel ends up a dead text with no
|
||||
* buttons. The same lists sent by /ban, /version… carry no such button, and their results stay bare.
|
||||
*/
|
||||
export function keepPanelBack(ctx: Context): { reply_markup?: InlineKeyboardMarkup } {
|
||||
const rows = (ctx.callbackQuery?.message as { reply_markup?: InlineKeyboardMarkup } | undefined)?.reply_markup?.inline_keyboard ?? [];
|
||||
const back = rows
|
||||
.flat()
|
||||
.find((b): b is InlineKeyboardButton.CallbackButton => 'callback_data' in b && b.callback_data.startsWith('tlmx_panel:') && b.text.startsWith('◀️'));
|
||||
return back ? { reply_markup: Markup.inlineKeyboard([[Markup.button.callback(back.text, back.callback_data)]]).reply_markup } : {};
|
||||
}
|
||||
|
|
@ -29,6 +29,7 @@ import { createBigFiles } from './bigFiles.js';
|
|||
import { canRenderAnimatedStickers } from './lottie.js';
|
||||
import { reportBridgeError } from './errorReporter.js';
|
||||
import { wireControlPanel, type PauseControl } from './panel.js';
|
||||
import { keepPanelBack } from './panelBack.js';
|
||||
import { createBugReports, isBugReportInboxEnabled, BUGREPORT_BOT_HANDLE, type BugReports } from './bugReports.js';
|
||||
import { createMaxAuthFlow, type MaxAuthCallbacks } from './maxAuthFlow.js';
|
||||
import { createTelemetry } from './telemetry.js';
|
||||
|
|
@ -2631,9 +2632,11 @@ export function wireBridge({
|
|||
// A fresh /version still shows a live «Обновить» button mid-update (the container isn't
|
||||
// rebuilt yet). update-requested = queued for the host watcher; update-in-progress = update.sh
|
||||
// is running. flock in update-watcher.sh is the hard backstop; this is the friendly heads-up.
|
||||
// Pressed in the panel, the result keeps its «◀️ Назад» (keepPanelBack).
|
||||
const back = keepPanelBack(ctx);
|
||||
if ((await markerActive(UPDATE_REQUESTED_MARKER)) || (await markerActive(UPDATE_IN_PROGRESS_MARKER))) {
|
||||
await ctx.answerCbQuery('Обновление уже идёт');
|
||||
await ctx.editMessageText('⏳ Обновление уже запущено — дождитесь сообщения о завершении.').catch(() => {});
|
||||
await ctx.editMessageText('⏳ Обновление уже запущено — дождитесь сообщения о завершении.', back).catch(() => {});
|
||||
return;
|
||||
}
|
||||
// The marker is consumed only by the host dispatcher (absent when setup.sh ran without root):
|
||||
|
|
@ -2641,17 +2644,17 @@ export function wireBridge({
|
|||
if (!(await watcherAlive())) {
|
||||
await ctx.answerCbQuery('Автообновление не настроено');
|
||||
await ctx
|
||||
.editMessageText('⚠️ Автообновление на этом мосту не настроено — запрос некому выполнить.\nОбновите на сервере: cd <каталог моста> && ./update.sh\n(автообновление ставится запуском ./setup.sh от root)')
|
||||
.editMessageText('⚠️ Автообновление на этом мосту не настроено — запрос некому выполнить.\nОбновите на сервере: cd <каталог моста> && ./update.sh\n(автообновление ставится запуском ./setup.sh от root)', back)
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
await ctx.answerCbQuery('Обновление запрошено');
|
||||
try {
|
||||
await writeFile(UPDATE_REQUESTED_MARKER, new Date().toISOString(), 'utf8');
|
||||
await ctx.editMessageText('⏳ Обновление запрошено — история переписки не затрагивается.');
|
||||
await ctx.editMessageText('⏳ Обновление запрошено — история переписки не затрагивается.', back);
|
||||
} catch (err) {
|
||||
logger.error('Failed to write update-requested marker', err);
|
||||
await ctx.editMessageText('❌ Не удалось запросить обновление — смотрите логи контейнера.');
|
||||
await ctx.editMessageText('❌ Не удалось запросить обновление — смотрите логи контейнера.', back);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2700,7 +2703,7 @@ export function wireBridge({
|
|||
|
||||
bot.action('tlmx_dismiss', async (ctx) => {
|
||||
await ctx.answerCbQuery('Ок');
|
||||
await ctx.editMessageText('⏰ Отложено — напомню при следующей ежедневной проверке.');
|
||||
await ctx.editMessageText('⏰ Отложено — напомню при следующей ежедневной проверке.', keepPanelBack(ctx));
|
||||
});
|
||||
|
||||
// /ban — mute a MAX chat: its messages stop being mirrored and its topic is deleted. Persistent,
|
||||
|
|
@ -2729,7 +2732,7 @@ export function wireBridge({
|
|||
await deleteTopic(mapping.telegramTopicId, '/ban');
|
||||
}
|
||||
await ctx.answerCbQuery('Забанен');
|
||||
await ctx.editMessageText(`🚫 Забанен: ${mapping?.title ?? maxChatId}. Сообщения больше не приходят. Вернуть — /unban.`).catch(() => {});
|
||||
await ctx.editMessageText(`🚫 Забанен: ${mapping?.title ?? maxChatId}. Сообщения больше не приходят. Вернуть — /unban.`, keepPanelBack(ctx)).catch(() => {});
|
||||
});
|
||||
|
||||
// /unban — flips the flag; the topic comes back on the next incoming message from that chat.
|
||||
|
|
@ -2752,7 +2755,7 @@ export function wireBridge({
|
|||
const mapping = await chatMapStore.getByMaxChatId(maxChatId);
|
||||
await chatMapStore.setBanned(maxChatId, false);
|
||||
await ctx.answerCbQuery('Разбанен');
|
||||
await ctx.editMessageText(`♻️ Разбанен: ${mapping?.title ?? maxChatId}. Тема вернётся при следующем сообщении от него.`).catch(() => {});
|
||||
await ctx.editMessageText(`♻️ Разбанен: ${mapping?.title ?? maxChatId}. Тема вернётся при следующем сообщении от него.`, keepPanelBack(ctx)).catch(() => {});
|
||||
});
|
||||
|
||||
// --- Control panel (src/bridge/panel.ts): a pinned inline-button menu in General. The leaves
|
||||
|
|
@ -2831,7 +2834,7 @@ export function wireBridge({
|
|||
pause,
|
||||
files: fileShare,
|
||||
resendToMax: (id) => bigFiles.resendToMax(id),
|
||||
offerStore: () => bigFiles.offerStore(),
|
||||
offerStore: (panelMessageId) => bigFiles.offerStore(panelMessageId),
|
||||
// Warm-cache name lookup (buildRoster already fetched the profiles); undefined on a miss lets
|
||||
// the panel fall back to CONTACT_INFO.
|
||||
resolveContactName: (uid) => {
|
||||
|
|
@ -2865,28 +2868,23 @@ export function wireBridge({
|
|||
});
|
||||
},
|
||||
leaves: {
|
||||
sendHelp: (chatId) => bot.telegram.sendMessage(chatId, buildHelpText()).then(() => {}),
|
||||
sendVersion: async (chatId) => {
|
||||
help: async () => ({ text: buildHelpText(), rows: [] }),
|
||||
version: async () => {
|
||||
const { text, replyMarkup } = formatVersionMessage(await checkVersion());
|
||||
await bot.telegram.sendMessage(chatId, text, { reply_markup: replyMarkup });
|
||||
return { text, rows: replyMarkup?.inline_keyboard ?? [] };
|
||||
},
|
||||
sendBanList: async (chatId) => {
|
||||
banList: async () => {
|
||||
const active = (await chatMapStore.list()).filter((m) => !m.banned);
|
||||
if (active.length === 0) {
|
||||
await bot.telegram.sendMessage(chatId, 'Нет активных чатов для бана.');
|
||||
return;
|
||||
}
|
||||
const buttons = active.slice(0, 90).map((m) => Markup.button.callback(m.title || `MAX chat ${m.maxChatId}`, `tlmx_ban:${m.maxChatId}`));
|
||||
await bot.telegram.sendMessage(chatId, '🚫 Кого забанить?', { reply_markup: Markup.inlineKeyboard(buttons, { columns: 1 }).reply_markup });
|
||||
if (active.length === 0) return { text: 'Нет активных чатов для бана.', rows: [] };
|
||||
return {
|
||||
text: '🚫 Кого забанить? Сообщения от выбранного чата приходить перестанут, его тема удалится.',
|
||||
rows: active.slice(0, 90).map((m) => [Markup.button.callback(m.title || `MAX chat ${m.maxChatId}`, `tlmx_ban:${m.maxChatId}`)]),
|
||||
};
|
||||
},
|
||||
sendUnbanList: async (chatId) => {
|
||||
unbanList: async () => {
|
||||
const banned = (await chatMapStore.list()).filter((m) => m.banned);
|
||||
if (banned.length === 0) {
|
||||
await bot.telegram.sendMessage(chatId, 'Забаненных чатов нет.');
|
||||
return;
|
||||
}
|
||||
const buttons = banned.slice(0, 90).map((m) => Markup.button.callback(m.title || `MAX chat ${m.maxChatId}`, `tlmx_unban:${m.maxChatId}`));
|
||||
await bot.telegram.sendMessage(chatId, '♻️ Кого разбанить?', { reply_markup: Markup.inlineKeyboard(buttons, { columns: 1 }).reply_markup });
|
||||
if (banned.length === 0) return { text: 'Забаненных чатов нет.', rows: [] };
|
||||
return { text: '♻️ Кого разбанить?', rows: banned.slice(0, 90).map((m) => [Markup.button.callback(m.title || `MAX chat ${m.maxChatId}`, `tlmx_unban:${m.maxChatId}`)]) };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
22
tests/panelBack.test.ts
Normal file
22
tests/panelBack.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { Context } from 'telegraf';
|
||||
import { keepPanelBack } from '../src/bridge/panelBack.js';
|
||||
|
||||
const pressedIn = (inline_keyboard: unknown[][]) => ({ callbackQuery: { message: { reply_markup: { inline_keyboard } } } }) as unknown as Context;
|
||||
|
||||
describe('keepPanelBack', () => {
|
||||
it('keeps the panel «◀️ Назад» of a list drawn in the panel', () => {
|
||||
const ctx = pressedIn([[{ text: 'Чат 1', callback_data: 'tlmx_ban:1' }], [{ text: '◀️ Назад', callback_data: 'tlmx_panel:chats' }]]);
|
||||
expect(keepPanelBack(ctx)).toEqual({ reply_markup: { inline_keyboard: [[{ text: '◀️ Назад', callback_data: 'tlmx_panel:chats', hide: false }]] } });
|
||||
});
|
||||
it('keeps «◀️ К файлам» under an upload link in the panel', () => {
|
||||
const ctx = pressedIn([[{ text: '📤 Загрузить файл', url: 'https://x/f/t' }], [{ text: '✖️ Отменить', callback_data: 'tlmx_panel:fcancel:t' }], [{ text: '◀️ К файлам', callback_data: 'tlmx_panel:files' }]]);
|
||||
expect(keepPanelBack(ctx).reply_markup?.inline_keyboard).toEqual([[{ text: '◀️ К файлам', callback_data: 'tlmx_panel:files', hide: false }]]);
|
||||
});
|
||||
it('adds nothing to the lists sent by /ban, /version or an upload link in a topic', () => {
|
||||
expect(keepPanelBack(pressedIn([[{ text: 'Чат 1', callback_data: 'tlmx_ban:1' }]]))).toEqual({});
|
||||
expect(keepPanelBack(pressedIn([[{ text: '🔄 Обновить', callback_data: 'tlmx_update' }, { text: '⏰ Позже', callback_data: 'tlmx_dismiss' }]]))).toEqual({});
|
||||
expect(keepPanelBack(pressedIn([[{ text: '📤 Загрузить файл', url: 'https://x/f/t' }], [{ text: '✖️ Отменить', callback_data: 'tlmx_panel:fcancel:t' }]]))).toEqual({});
|
||||
expect(keepPanelBack({} as Context)).toEqual({});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue