mirror of
https://github.com/Trollobot/Telemax.git
synced 2026-10-19 02:20:58 +00:00
1.3.6-beta.1: фото из MAX, которое Telegram не принимает как фото (HEIC/TIFF/AVIF, больше 10 000 px по сумме сторон или 20:1 — MAX пускает до 7680×7680), приходит файлом, а не «[фото]»; расширение по содержимому, а не всегда .jpg; формат и причина отказа в логе; источник пересылки — имя чата как у темы (личный диалог — собеседник), без «MAX chat N» и без повтора отправителя
This commit is contained in:
parent
d151900db3
commit
6f89576896
6 changed files with 88 additions and 14 deletions
|
|
@ -216,11 +216,34 @@ export async function downloadMaxAttachment(att: MaxAttachment, ctx: DownloadCon
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An image's real format from its first bytes — MAX takes JPG, PNG, GIF, TIFF, BMP and HEIC (its
|
||||
* upload docs), Telegram's sendPhoto refuses some of them, and a HEIC named `.jpg` opens nowhere.
|
||||
* 'jpg' when nothing matches. Pure + exported for unit testing.
|
||||
*/
|
||||
export function imageExtension(buf: Uint8Array): string {
|
||||
const ascii = (from: number, to: number) => String.fromCharCode(...buf.subarray(from, to));
|
||||
if (buf[0] === 0x89 && ascii(1, 4) === 'PNG') return 'png';
|
||||
if (ascii(0, 4) === 'GIF8') return 'gif';
|
||||
if (ascii(0, 4) === 'RIFF' && ascii(8, 12) === 'WEBP') return 'webp';
|
||||
if (ascii(0, 2) === 'BM') return 'bmp';
|
||||
if (ascii(0, 4) === 'II*\0' || ascii(0, 4) === 'MM\0*') return 'tiff';
|
||||
if (ascii(4, 8) === 'ftyp') {
|
||||
const brand = ascii(8, 12);
|
||||
if (brand === 'avif' || brand === 'avis') return 'avif';
|
||||
if (['heic', 'heix', 'hevc', 'hevx', 'heim', 'heis', 'mif1', 'msf1'].includes(brand)) return 'heic';
|
||||
}
|
||||
return 'jpg';
|
||||
}
|
||||
|
||||
/** Image formats Telegram doesn't take as a photo — they go as a file straight away instead of a doomed sendPhoto. */
|
||||
export const PHOTO_AS_FILE = new Set(['heic', 'tiff', 'avif']);
|
||||
|
||||
async function downloadAttachmentOrThrow(att: MaxAttachment, ctx: DownloadContext): Promise<DownloadedAttachment | null> {
|
||||
if (att._type === 'PHOTO' && att.baseUrl && att.photoToken) {
|
||||
const buffer = await downloadUrl(att.baseUrl + att.photoToken);
|
||||
if (!buffer) return null;
|
||||
return { buffer, filename: `photo_${String(att.photoId ?? Date.now())}.jpg`, kind: 'photo' };
|
||||
return { buffer, filename: `photo_${String(att.photoId ?? Date.now())}.${imageExtension(buffer)}`, kind: 'photo' };
|
||||
}
|
||||
|
||||
// Animated stickers carry a `lottieUrl`: gzip-compressed Lottie JSON that is already a valid
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
KEYBOARD_GEO,
|
||||
KEYBOARD_CONTACT,
|
||||
pressableButton,
|
||||
PHOTO_AS_FILE,
|
||||
type MaxAttachment,
|
||||
type DownloadContext,
|
||||
} from './attachments.js';
|
||||
|
|
@ -697,7 +698,21 @@ async function sendOneAttachment(
|
|||
return withFloodRetry(() => bot.telegram.sendMessage(groupId, describeAttachment(att), opts));
|
||||
}
|
||||
const source = { source: downloaded.buffer, filename: downloaded.filename };
|
||||
if (kind === 'photo') return withFloodRetry(() => bot.telegram.sendPhoto(groupId, source, opts));
|
||||
if (kind === 'photo') {
|
||||
// MAX takes images Telegram won't show as a photo: HEIC/TIFF (format), or past sendPhoto's
|
||||
// 10 000 px (width + height) or 20:1 — MAX allows 7680×7680. Such a picture goes as a file
|
||||
// instead of a «[фото]» placeholder (seen live 2026-10-13 on a user's bridge).
|
||||
const ext = downloaded.filename.split('.').pop() ?? 'jpg';
|
||||
if (ext !== 'jpg') logger.info(`MAX photo arrived as ${ext} (${downloaded.buffer.byteLength} bytes)`);
|
||||
if (PHOTO_AS_FILE.has(ext)) return withFloodRetry(() => bot.telegram.sendDocument(groupId, source, opts));
|
||||
try {
|
||||
return await withFloodRetry(() => bot.telegram.sendPhoto(groupId, source, opts));
|
||||
} catch (err) {
|
||||
if (!isPermanentTelegramRefusal(err)) throw err;
|
||||
logger.error(`Telegram refused a MAX photo (${ext}, ${downloaded.buffer.byteLength} bytes: ${telegramErrorText(err)}) — sending it as a file`);
|
||||
return withFloodRetry(() => bot.telegram.sendDocument(groupId, source, opts));
|
||||
}
|
||||
}
|
||||
if (kind === 'video') return withFloodRetry(() => bot.telegram.sendVideo(groupId, source, opts));
|
||||
if (kind === 'video_note') {
|
||||
try {
|
||||
|
|
@ -835,6 +850,7 @@ export async function resolveForwardContent(
|
|||
wrapperChatId: unknown,
|
||||
wrapperMessageId: unknown,
|
||||
profiles?: Map<number, ContactProfile>,
|
||||
myAccountId: number | null = null,
|
||||
): Promise<{ text: string; attaches: MaxAttachment[]; download: ForwardDownloadIds } | null> {
|
||||
if (link?.type !== 'FORWARD') return null;
|
||||
const original = link.message;
|
||||
|
|
@ -843,11 +859,13 @@ export async function resolveForwardContent(
|
|||
const senderName = Number.isNaN(senderId)
|
||||
? 'неизвестно'
|
||||
: resolveContactDisplayName(senderId, await resolveProfile(max, senderId, 'forward sender', profiles));
|
||||
const sourceChat = chats.find((c) => c && typeof c === 'object' && String((c as { id?: unknown }).id) === String(link.chatId)) as
|
||||
| { title?: string }
|
||||
| undefined;
|
||||
const sourceLabel = sourceChat?.title || `MAX chat ${String(link.chatId)}`;
|
||||
const prefix = `↩️ Переслано из «${sourceLabel}» (от ${senderName}):`;
|
||||
// The source chat is named the way its topic is: a 1:1 dialog has no title of its own — it is the
|
||||
// other person (seen live 2026-10-13 as «MAX chat 381718116»). No real name, or just the sender
|
||||
// again (a forward out of a dialog with them) — the «из «…»» part is left out.
|
||||
const sourceChat = chats.find((c) => c && typeof c === 'object' && String((c as { id?: unknown }).id) === String(link.chatId));
|
||||
const sourceName = sourceChat ? resolveChatName(sourceChat, myAccountId, profiles ?? new Map()) : undefined;
|
||||
const sourceLabel = sourceName && !isFallbackTitle(sourceName) && sourceName !== senderName ? sourceName : undefined;
|
||||
const prefix = sourceLabel ? `↩️ Переслано из «${sourceLabel}» (от ${senderName}):` : `↩️ Переслано (от ${senderName}):`;
|
||||
return {
|
||||
text: original?.text ? `${prefix}\n${original.text}` : prefix,
|
||||
attaches,
|
||||
|
|
@ -1146,7 +1164,7 @@ export class ChatCatchUp {
|
|||
await chatMapStore.advanceHistoryCursor(chatId, msg.time);
|
||||
continue;
|
||||
}
|
||||
const forwarded = await resolveForwardContent(max, chats, msg.link, chatId, msg.id, profiles);
|
||||
const forwarded = await resolveForwardContent(max, chats, msg.link, chatId, msg.id, profiles, myAccountId);
|
||||
let text = forwarded ? forwarded.text : msg.text;
|
||||
const taken = takeKeyboard(forwarded ? forwarded.attaches : Array.isArray(msg.attaches) ? (msg.attaches as MaxAttachment[]) : [], chatId);
|
||||
let attaches = taken.attaches;
|
||||
|
|
@ -2257,7 +2275,7 @@ export function wireBridge({
|
|||
if (!opened || opened.deferred) return;
|
||||
const topicId = opened.topicId;
|
||||
|
||||
const forwarded = await resolveForwardContent(max, getChats(), message.link, chatId, message.id, getContactProfiles());
|
||||
const forwarded = await resolveForwardContent(max, getChats(), message.link, chatId, message.id, getContactProfiles(), getMyAccountId());
|
||||
if (forwarded) {
|
||||
text = forwarded.text;
|
||||
attaches = forwarded.attaches;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue