mirror of
https://github.com/Trollobot/Telemax.git
synced 2026-10-19 10:23:24 +00:00
1.3.7-beta.1: сторож Telegram — запрос новых сообщений висит дольше 2 минут (у Telegraf таймаут 500 с) → обрыв и переподключение, повторно за 10 минут → перезапуск контейнера, отчёт telegram-poll-stall; «🔁 Перезапустить мост» в пульте → Система (без пересборки, пульт возвращается сам); админу моста в личке — подсказка «команды в группе» с ссылкой вместо заглушки баг-бота; HISTORY_LIMIT — при первой синхронизации только последние N сообщений чата, с пометкой в теме, вопрос в установщике, строка в меню настроек
This commit is contained in:
parent
7e50dcd810
commit
e6705ad0ed
10 changed files with 322 additions and 14 deletions
|
|
@ -48,6 +48,8 @@ export interface ControlPanelDeps {
|
|||
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 }>;
|
||||
/** «🔁 Перезапустить мост»: ends the process for Docker to start it again (server/app.ts); `where` = the panel message to put back afterwards. */
|
||||
restart?: (where: { chatId: number; messageId: number }) => void;
|
||||
}
|
||||
|
||||
export interface PauseControl {
|
||||
|
|
@ -342,20 +344,32 @@ function webView(botUsername?: string): View {
|
|||
markup: Markup.inlineKeyboard(rows).reply_markup,
|
||||
};
|
||||
}
|
||||
function systemView(paused: boolean): View {
|
||||
/** «⚙️ Система» — exported for the bridge that comes back from «🔁 Перезапустить мост» (server/app.ts), with `note` under the title. */
|
||||
export function systemView(paused: boolean, note?: string): View {
|
||||
const pauseBtn = paused
|
||||
? Markup.button.callback('▶️ Возобновить MAX', 'tlmx_panel:resume')
|
||||
: Markup.button.callback('⏸ Пауза MAX', 'tlmx_panel:pause');
|
||||
return {
|
||||
text: '⚙️ Система:',
|
||||
text: note ? `⚙️ Система:\n${note}` : '⚙️ Система:',
|
||||
markup: Markup.inlineKeyboard([
|
||||
[Markup.button.callback('📊 Статус', 'tlmx_panel:status')],
|
||||
[pauseBtn, Markup.button.callback('⬆️ Обновление', 'tlmx_panel:update')],
|
||||
[Markup.button.callback('🔄 Пересинхронизация', 'tlmx_panel:resync'), Markup.button.callback('📋 Команды', 'tlmx_panel:help')],
|
||||
[Markup.button.callback('🔁 Перезапустить мост', 'tlmx_panel:restart')],
|
||||
[Markup.button.callback('◀️ Назад', 'tlmx_panel:root')],
|
||||
]).reply_markup,
|
||||
};
|
||||
}
|
||||
function restartConfirmView(): View {
|
||||
return {
|
||||
text:
|
||||
'🔁 Перезапустить мост?\nКонтейнер перезапустится без пересборки — примерно за 10–20 секунд. Переписка не пострадает: что придёт за это время, догрузится само. Загрузка большого файла, если она идёт прямо сейчас, прервётся.\nПомогает, если мост ведёт себя странно. Не помог — пришлите /panel → 📊 Статус и логи разработчику.',
|
||||
markup: Markup.inlineKeyboard([
|
||||
[Markup.button.callback('🔁 Да, перезапустить', 'tlmx_panel:restart:go')],
|
||||
[Markup.button.callback('◀️ Назад', 'tlmx_panel:system')],
|
||||
]).reply_markup,
|
||||
};
|
||||
}
|
||||
function pauseView(): View {
|
||||
return {
|
||||
text: '⏸ На сколько поставить MAX на паузу? Приём/отправка остановятся, авто-возобновление по таймеру.\nПауза живёт в памяти: перезапуск моста (в том числе автообновление) снимет её в любом случае.',
|
||||
|
|
@ -368,7 +382,7 @@ function pauseView(): View {
|
|||
}
|
||||
|
||||
export function wireControlPanel(deps: ControlPanelDeps): void {
|
||||
const { bot, targetGroupId, max, getActivePhone, triggerFullResync, leaves, startDialog, resolveContactName, getStatus, pause, files, resendToMax, offerStore } = deps;
|
||||
const { bot, targetGroupId, max, getActivePhone, triggerFullResync, leaves, startDialog, resolveContactName, getStatus, pause, files, resendToMax, offerStore, restart } = deps;
|
||||
const card = (f: StoredFile, link?: string): View => fileCardView(f, files!, link, Boolean(resendToMax));
|
||||
const root = (): View => rootView(getActivePhone(), pause.until());
|
||||
const system = (): View => systemView(pause.until() != null);
|
||||
|
|
@ -595,6 +609,25 @@ export function wireControlPanel(deps: ControlPanelDeps): void {
|
|||
void triggerFullResync().catch((err) => logger.error('panel resync failed', err));
|
||||
});
|
||||
|
||||
// «🔁 Перезапустить мост»: the process ends and Docker starts it again — no rebuild, no SSH. The
|
||||
// way to unstick a bridge while it still answers (a user asked for one after a host reboot,
|
||||
// 2026-10-13); a deaf one is the polling watchdog's job (server/app.ts).
|
||||
bot.action('tlmx_panel:restart', async (ctx) => {
|
||||
await ctx.answerCbQuery().catch(() => {});
|
||||
await edit(ctx, restartConfirmView());
|
||||
});
|
||||
bot.action('tlmx_panel:restart:go', async (ctx) => {
|
||||
const chatId = ctx.callbackQuery.message?.chat.id;
|
||||
const messageId = ctx.callbackQuery.message?.message_id;
|
||||
if (!restart || chatId == null || messageId == null) {
|
||||
await ctx.answerCbQuery('Перезапуск отсюда недоступен — docker compose restart в папке моста', { show_alert: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
await ctx.answerCbQuery('Перезапускаю…').catch(() => {});
|
||||
await ctx.editMessageText('🔁 Перезапускаю мост… Вернусь через 10–20 секунд — этот пульт обновится сам.').catch(() => {});
|
||||
restart({ chatId, messageId });
|
||||
});
|
||||
|
||||
// «📊 Статус»: server/bridge health in one card (see status.ts for what is and isn't visible).
|
||||
bot.action('tlmx_panel:status', async (ctx) => {
|
||||
await ctx.answerCbQuery().catch(() => {});
|
||||
|
|
|
|||
|
|
@ -774,18 +774,23 @@ export function historyStartTime(now: number = Date.now()): number {
|
|||
* (batches can re-include the boundary message), oldest-first. With `sinceTime` only strictly
|
||||
* newer messages are returned and paging stops once a batch's oldest is at/before it — the cheap
|
||||
* "what's new since last time" every catch-up runs (a push arriving between TCP sessions is
|
||||
* otherwise lost forever — hit live 2026-08-12, mid-redeploy).
|
||||
* otherwise lost forever — hit live 2026-08-12, mid-redeploy). `limit` (a chat filled from scratch,
|
||||
* HISTORY_LIMIT) stops paging once that many are in hand and keeps the newest; `truncated` says
|
||||
* older messages were left in MAX.
|
||||
*/
|
||||
async function fetchFullHistory(
|
||||
max: MaxClient,
|
||||
chatId: unknown,
|
||||
sinceTime: number | null = null,
|
||||
isCancelled?: () => boolean,
|
||||
): Promise<MaxHistoryMessage[]> {
|
||||
limit = 0,
|
||||
): Promise<{ messages: MaxHistoryMessage[]; truncated: boolean }> {
|
||||
const all: MaxHistoryMessage[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
let fromTime = historyStartTime();
|
||||
for (let i = 0; i < HISTORY_MAX_BATCHES; i++) {
|
||||
// One more than the limit tells «there is older history» from «the chat is exactly this long».
|
||||
if (limit > 0 && all.length > limit) break;
|
||||
// A big chat pages for a minute or more — /reboot and /kill wait for the run to stop.
|
||||
if (isCancelled?.()) throw new SyncCancelledError();
|
||||
const batch = await max.getChatHistory(chatId, fromTime, HISTORY_BATCH_SIZE);
|
||||
|
|
@ -808,7 +813,41 @@ async function fetchFullHistory(
|
|||
fromTime = oldestTime;
|
||||
}
|
||||
all.sort((a, b) => a.time - b.time);
|
||||
return all;
|
||||
const truncated = limit > 0 && all.length > limit;
|
||||
return { messages: truncated ? all.slice(-limit) : all, truncated };
|
||||
}
|
||||
|
||||
/** What the bridge's admin reads when writing to the bot's DM (anything but /login). Pure + exported for unit testing. */
|
||||
export function adminDmHintText(groupTitle: string): string {
|
||||
return [
|
||||
'👋 Это ваш мост Telemax. Здесь, в личке, работает только вход в MAX — /login.',
|
||||
'',
|
||||
`Всё остальное — в группе моста${groupTitle ? ` «${groupTitle}»` : ''}:`,
|
||||
'• /panel — пульт, в общей теме',
|
||||
'• /info, /file, /broadcast — в теме нужного чата',
|
||||
'• /help — все команды',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/** A members' link to the bridge's supergroup: https://t.me/c/<id without -100>. */
|
||||
export function bridgeGroupLink(groupId: string): string {
|
||||
return `https://t.me/c/${groupId.replace(/^-100/, '')}`;
|
||||
}
|
||||
|
||||
/** The first line of a topic HISTORY_LIMIT cut short: «последние 300 сообщений» (agreed with the number). */
|
||||
export function historyLimitNotice(limit: number): string {
|
||||
const d = limit % 10;
|
||||
const dd = limit % 100;
|
||||
const one = d === 1 && dd !== 11;
|
||||
const few = d >= 2 && d <= 4 && (dd < 12 || dd > 14);
|
||||
const phrase = one ? `последнее ${limit} сообщение` : `последние ${limit} ${few ? 'сообщения' : 'сообщений'}`;
|
||||
return `📜 Здесь ${phrase} этого чата — так задано при установке (HISTORY_LIMIT в .env). Более ранняя переписка осталась в MAX.`;
|
||||
}
|
||||
|
||||
/** HISTORY_LIMIT from .env: how many of a chat's newest messages a topic filled from scratch gets (0 = all). */
|
||||
export function historyLimitFromEnv(env: NodeJS.ProcessEnv = process.env): number {
|
||||
const n = Number((env.HISTORY_LIMIT ?? '').trim());
|
||||
return Number.isInteger(n) && n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
/** A received forward's `link` — the same shape on PUSH_MESSAGE and in CHAT_HISTORY. */
|
||||
|
|
@ -952,6 +991,8 @@ export interface ChatCatchUpDeps {
|
|||
paceMs?: number;
|
||||
/** A message the catch-up put into its topic — «трансляция» copies it on (broadcast.ts; its cursor drops replays). */
|
||||
onRelayed?: (c: IncomingCopy) => void;
|
||||
/** HISTORY_LIMIT: newest messages a topic filled from scratch gets (0 / unset = the whole history). */
|
||||
historyLimit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1093,9 +1134,17 @@ export class ChatCatchUp {
|
|||
*/
|
||||
private async replay(chatId: unknown, topicId: number, cursor: number | null, cancelled: () => boolean): Promise<void> {
|
||||
const session = this.session;
|
||||
const history = await fetchFullHistory(this.deps.max, chatId, cursor, cancelled);
|
||||
// HISTORY_LIMIT caps only a topic filled from scratch; a catch-up from a cursor takes everything new.
|
||||
const limit = cursor == null ? (this.deps.historyLimit ?? 0) : 0;
|
||||
const { messages: history, truncated } = await fetchFullHistory(this.deps.max, chatId, cursor, cancelled, limit);
|
||||
if (history.length > 0) {
|
||||
logger.info(`${cursor == null ? 'Backfilling' : 'Catching up on'} ${history.length} messages for MAX chat ${String(chatId)}`);
|
||||
logger.info(
|
||||
`${cursor == null ? 'Backfilling' : 'Catching up on'} ${history.length} messages for MAX chat ${String(chatId)}${truncated ? ` (the newest ${limit} — HISTORY_LIMIT)` : ''}`,
|
||||
);
|
||||
// Says why the topic starts mid-conversation — a 5 000-message chat needs ~4 hours in full.
|
||||
if (truncated) {
|
||||
await sendTextPieces(this.deps.bot, this.deps.groupId, topicId, historyLimitNotice(limit));
|
||||
}
|
||||
await this.backfill(chatId, topicId, history, cancelled);
|
||||
}
|
||||
if (session === this.session) this.caughtUp.add(String(chatId));
|
||||
|
|
@ -1473,6 +1522,8 @@ export interface BridgeOptions {
|
|||
auth: MaxAuthCallbacks;
|
||||
/** The panel's MAX pause (server/maxSession.ts). */
|
||||
pause: PauseControl;
|
||||
/** The panel's «🔁 Перезапустить мост»: ends the process for Docker to start again (server/app.ts). */
|
||||
restartBridge?: (where: { chatId: number; messageId: number }) => void;
|
||||
}
|
||||
|
||||
/** Wires MAX push messages <-> Telegram forum topics in both directions (ТЗ.md §1.2). */
|
||||
|
|
@ -1496,6 +1547,7 @@ export function wireBridge({
|
|||
suspendChatSync,
|
||||
auth,
|
||||
pause,
|
||||
restartBridge,
|
||||
}: BridgeOptions): WiredBridge {
|
||||
// Bug-report channel: private DMs from outsiders become bug reports. The inbox is only ON where
|
||||
// BUGREPORT_INBOX is set (the maintainer's prod bot); elsewhere outsiders get a redirect stub.
|
||||
|
|
@ -1554,6 +1606,7 @@ export function wireBridge({
|
|||
// A broadcast's chat picker, then the /login auth flow get first refusal (both admin-gate internally).
|
||||
if (await broadcasts.handlePrivate(ctx)) return;
|
||||
if (await maxAuth.handlePrivate(ctx)) return;
|
||||
if (await adminDmHint(ctx)) return;
|
||||
await bugReports.handleIncomingPrivate(ctx);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1611,6 +1664,25 @@ export function wireBridge({
|
|||
}
|
||||
});
|
||||
|
||||
// The bridge's own admin writing to the bot's DM is told where the bridge lives instead of getting
|
||||
// the outsiders' bug-report redirect: a user typed /panel, /info, /broadcast here and got «пишите в
|
||||
// баг-бот» every time, and took the bridge for broken (2026-10-13). Only /login works in the DM.
|
||||
let bridgeGroupTitle: string | undefined;
|
||||
async function adminDmHint(ctx: Context): Promise<boolean> {
|
||||
const userId = ctx.from?.id;
|
||||
if (userId == null || !ctx.message || !(await isGroupAdmin(userId).catch(() => false))) return false;
|
||||
if (bridgeGroupTitle === undefined) {
|
||||
const chat = await bot.telegram.getChat(targetGroupId).catch(() => undefined);
|
||||
bridgeGroupTitle = chat && 'title' in chat ? chat.title : '';
|
||||
}
|
||||
await ctx
|
||||
.reply(adminDmHintText(bridgeGroupTitle), {
|
||||
reply_markup: Markup.inlineKeyboard([[Markup.button.url('➡️ Открыть группу моста', bridgeGroupLink(targetGroupId))]]).reply_markup,
|
||||
})
|
||||
.catch((err) => logger.error('Failed to send the admin DM hint', err));
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Creator/administrator of the bridge group — the one notion of "admin" for commands, buttons and the 👎 delete gesture. Throws when Telegram can't be asked; callers fail closed. */
|
||||
async function isGroupAdmin(userId: number): Promise<boolean> {
|
||||
const member = await bot.telegram.getChatMember(targetGroupId, userId);
|
||||
|
|
@ -1640,6 +1712,7 @@ export function wireBridge({
|
|||
forgetChatLinks,
|
||||
isWiping: () => wiping,
|
||||
onRelayed: (c) => broadcasts.copyIncoming(c),
|
||||
historyLimit: historyLimitFromEnv(),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -2925,6 +2998,7 @@ export function wireBridge({
|
|||
triggerFullResync,
|
||||
startDialog,
|
||||
pause,
|
||||
restart: restartBridge,
|
||||
files: fileShare,
|
||||
resendToMax: (id) => bigFiles.resendToMax(id),
|
||||
offerStore: (panelMessageId) => bigFiles.offerStore(panelMessageId),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { dialogParticipantIds, extractMyAccountId, resolveChatName, type Contact
|
|||
import { SessionStore } from '../store/sessionStore.js';
|
||||
import { ChatMapStore } from '../store/chatMapStore.js';
|
||||
import { wireBridge, syncAllChatsToTelegram, type ChatCatchUp } from '../bridge/sync.js';
|
||||
import { systemView } from '../bridge/panel.js';
|
||||
import { RetryBackoff } from '../bridge/catchUp.js';
|
||||
import { isStaleCallbackQuery, isTransientTelegramError } from '../bridge/transient.js';
|
||||
import { configureErrorReporter, reportBridgeError } from '../bridge/errorReporter.js';
|
||||
|
|
@ -381,8 +382,10 @@ async function startServer(): Promise<void> {
|
|||
stop: () => maxSession.unpause(),
|
||||
until: () => maxSession.pausedUntil,
|
||||
},
|
||||
restartBridge: (where) => void restartFromPanel(where),
|
||||
}));
|
||||
chatSync.setRetryHandler(scheduleCatchUpRetry);
|
||||
watchPolling(bot);
|
||||
launchTelegramBotWithRetry(bot);
|
||||
// Route throttled operator error notices (MAX down, delivery failures, internal
|
||||
// errors) to the target group. Captured in a const so the closure keeps the
|
||||
|
|
@ -545,6 +548,96 @@ async function announceGroupReadyOnce(bot: Telegraf): Promise<void> {
|
|||
// delay starts over instead of staying at the 60 s cap accumulated over the process's life.
|
||||
const TELEGRAM_STABLE_POLLING_MS = 10 * 60_000;
|
||||
|
||||
// --- Telegram polling watchdog -------------------------------------------------------------------
|
||||
// Telegraf gives every API call 500 s (hard-coded), and getUpdates is a ≤50 s long poll. When its
|
||||
// connection dies quietly — a local proxy that came up after the bridge, a NAT dropping it — the
|
||||
// bridge looked connected but was deaf for up to 8 minutes (a user's home server after a reboot,
|
||||
// 2026-10-13). A poll in flight past POLL_STALL_MS is cut and polling relaunched; a second stall
|
||||
// soon after that ends the process and Docker (restart: unless-stopped) starts it afresh.
|
||||
const POLL_STALL_MS = 120_000;
|
||||
const POLL_RESTALL_WINDOW_MS = 10 * 60_000;
|
||||
let pollInFlightSince: number | null = null;
|
||||
let lastPollRelaunchAt = 0;
|
||||
|
||||
/**
|
||||
* Notes when a polling getUpdates goes out and when it settles — the watchdog's only signal. Only the
|
||||
* long polls (`timeout` > 0): a stopped Telegraf also sends a short `{offset, limit: 1}` one with no
|
||||
* abort signal, which on a dead connection would hang and look like a second stall.
|
||||
*/
|
||||
function watchPolling(bot_: Telegraf): void {
|
||||
const callApi = bot_.telegram.callApi.bind(bot_.telegram);
|
||||
bot_.telegram.callApi = ((method: string, payload: unknown, extra?: unknown) => {
|
||||
const longPoll = method === 'getUpdates' && Number((payload as { timeout?: unknown } | null)?.timeout) > 0;
|
||||
if (!longPoll) return (callApi as (m: string, p: unknown, e?: unknown) => Promise<unknown>)(method, payload, extra);
|
||||
pollInFlightSince = Date.now();
|
||||
return (callApi as (m: string, p: unknown, e?: unknown) => Promise<unknown>)(method, payload, extra).finally(() => {
|
||||
pollInFlightSince = null;
|
||||
});
|
||||
}) as typeof bot_.telegram.callApi;
|
||||
setInterval(() => {
|
||||
if (pollInFlightSince == null || Date.now() - pollInFlightSince < POLL_STALL_MS) return;
|
||||
const stalledFor = Math.round((Date.now() - pollInFlightSince) / 1000);
|
||||
pollInFlightSince = null;
|
||||
if (Date.now() - lastPollRelaunchAt < POLL_RESTALL_WINDOW_MS) {
|
||||
logger.error(`Telegram polling stalled again (${stalledFor}s without an answer) soon after a reconnect — restarting the bridge`);
|
||||
void reportError({ kind: 'internal', step: 'telegram-poll-stall', error: new Error(`getUpdates hung ${stalledFor}s twice — restart`) });
|
||||
exitForRestart(1);
|
||||
return;
|
||||
}
|
||||
lastPollRelaunchAt = Date.now();
|
||||
logger.error(`Telegram polling stalled: getUpdates unanswered for ${stalledFor}s — cutting it and reconnecting`);
|
||||
void reportError({ kind: 'internal', step: 'telegram-poll-stall', error: new Error(`getUpdates hung ${stalledFor}s — reconnected`) });
|
||||
try {
|
||||
bot_.stop('watchdog'); // aborts the hung request; that launch() then resolves quietly
|
||||
} catch {
|
||||
// not running — the relaunch below starts it
|
||||
}
|
||||
// After the stopped loop's own short getUpdates (offset sync): two at once get a 409 from Telegram.
|
||||
setTimeout(() => void launchTelegramBotWithRetry(bot_), 5_000);
|
||||
}, 30_000).unref();
|
||||
}
|
||||
|
||||
/** Ends the process for Docker (restart: unless-stopped) to start it again — the panel's «Перезапустить» and the watchdog's last resort. */
|
||||
function exitForRestart(code: number): void {
|
||||
try {
|
||||
maxSession.shutdown();
|
||||
} catch {
|
||||
// exiting anyway
|
||||
}
|
||||
try {
|
||||
bot?.stop('restart');
|
||||
} catch {
|
||||
// not running
|
||||
}
|
||||
setTimeout(() => process.exit(code), 800);
|
||||
}
|
||||
|
||||
// «🔁 Перезапустить мост» (panel → Система): where the press came from, so the bridge that comes
|
||||
// back can put the panel back with «✅ Мост перезапущен» instead of leaving «Перезапускаю…».
|
||||
const RESTART_REQUESTED_MARKER = path.join(process.cwd(), '.data', 'restart-requested');
|
||||
|
||||
async function restartFromPanel(where: { chatId: number; messageId: number }): Promise<void> {
|
||||
logger.info('Restart requested from the panel');
|
||||
await writeFile(RESTART_REQUESTED_MARKER, JSON.stringify({ ...where, at: Date.now() })).catch((err) => logger.error('Failed to write the restart marker', err));
|
||||
exitForRestart(0);
|
||||
}
|
||||
|
||||
async function reportIfJustRestarted(bot_: Telegraf): Promise<void> {
|
||||
let marker: { chatId?: number; messageId?: number; at?: number };
|
||||
try {
|
||||
marker = JSON.parse(await readFile(RESTART_REQUESTED_MARKER, 'utf8')) as typeof marker;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
await unlink(RESTART_REQUESTED_MARKER).catch(() => {});
|
||||
// A marker older than a few minutes is left over from a restart that never came back that way.
|
||||
if (marker.chatId == null || marker.messageId == null || Date.now() - (marker.at ?? 0) > 10 * 60_000) return;
|
||||
const { text, markup } = systemView(maxSession.pausedUntil != null, '✅ Мост перезапущен — всё работает.');
|
||||
await bot_.telegram
|
||||
.editMessageText(marker.chatId, marker.messageId, undefined, text, { reply_markup: markup })
|
||||
.catch((err) => logger.error('Failed to put the panel back after a restart', err));
|
||||
}
|
||||
|
||||
function retryTelegramLaunch(bot: Telegraf, attempt: number, reason: unknown): void {
|
||||
tgActive = false;
|
||||
const idx = Math.min(attempt, TELEGRAM_RETRY_DELAYS_MS.length - 1);
|
||||
|
|
@ -578,6 +671,7 @@ async function launchTelegramBotWithRetry(bot: Telegraf, attempt = 0): Promise<v
|
|||
bot.telegram.setMyDescription(buildBotDescription()).catch((err) => logger.error('Failed to set bot description', err));
|
||||
bot.telegram.setMyCommands(BOT_COMMANDS).catch((err) => logger.error('Failed to set bot commands', err));
|
||||
void reportIfJustUpdated(bot);
|
||||
void reportIfJustRestarted(bot);
|
||||
// SessionStore parked an unreadable session file (corrupt, or encrypted with a different
|
||||
// MAX_SESSION_KEY — the usual cause is restoring ./data without carrying .env across). The bridge
|
||||
// is up but unauthenticated, so say so in the group with the same re-auth prompt a rejected
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue