fix(route/telegram): fall back to the web preview on MTProto flood waits

The MTProto path cached the resolved input entity for the default 5
minutes, so every process that started later or missed the cache called
contacts.ResolveUsername again for each channel. That method is heavily
rate limited, and once the account hit a flood wait the route failed
with "A wait of N seconds is required" even for channels that the public
web preview serves fine.

Keep the channel ID and access hash for 30 days, since they do not
change, and fall back to the web preview when MTProto raises a
FloodWaitError. Other MTProto errors are still reported.

Closes #20454
This commit is contained in:
pseudoyu
2026-10-14 23:08:00 +08:00
parent d7a84ed3c7
commit 09594934e2
2 changed files with 13 additions and 2 deletions
+11 -1
View File
@@ -3,11 +3,13 @@ import querystring from 'node:querystring';
import { load } from 'cheerio';
import { FetchError } from 'ofetch';
import pMap from 'p-map';
import { errors } from 'teleproto';
import { config } from '@/config';
import type { DataItem, Route } from '@/types';
import { ViewType } from '@/types';
import cache from '@/utils/cache';
import logger from '@/utils/logger';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import { fallback, queryToBoolean } from '@/utils/readable-social';
@@ -159,7 +161,15 @@ When a reverse proxy removes a public URL prefix (for example, \`/rss\`), set it
async function handler(ctx) {
const useWeb = ctx.req.param('routeParams') || !config.telegram.session;
if (!useWeb) {
return tglibchannel(ctx);
try {
return await tglibchannel(ctx);
} catch (error) {
// A flood wait blocks the account for minutes to hours, while the public web preview still works
if (!(error instanceof errors.FloodWaitError)) {
throw error;
}
logger.warn(`Telegram MTProto requires a wait of ${error.seconds} seconds, falling back to the web preview`);
}
}
const username = ctx.req.param('username');
+2 -1
View File
@@ -148,7 +148,8 @@ export default async function handler(ctx: Context) {
if (!peerCache) {
const p = await client.getInputEntity(username!);
peerCache = JSON.stringify(p.toJSON());
await cache.set(`telegram:inputEntity:${username}`, peerCache);
// The channel ID and access hash do not change, and contacts.ResolveUsername is heavily rate limited
await cache.set(`telegram:inputEntity:${username}`, peerCache, 30 * 24 * 60 * 60);
}
const peerData = JSON.parse(peerCache, (k, v) => (k === 'channelId' || k === 'accessHash' ? returnBigInt(v) : v));
const peer = new Api.InputPeerChannel(peerData);