diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index e2a8484b8..c8fb4b732 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -21,7 +21,7 @@ type GramJsAppConfig = { function buildEmojiSounds(appConfig: GramJsAppConfig) { const { emojies_sounds } = appConfig; - return Object.keys(emojies_sounds).reduce((acc: Record, key) => { + return emojies_sounds ? Object.keys(emojies_sounds).reduce((acc: Record, key) => { const l = emojies_sounds[key]; localDb.documents[l.id] = new GramJs.Document({ id: BigInt(l.id), @@ -35,7 +35,7 @@ function buildEmojiSounds(appConfig: GramJsAppConfig) { acc[key] = l.id; return acc; - }, {}); + }, {}) : {}; } export function buildApiConfig(json: GramJs.TypeJSONValue): ApiAppConfig { diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index abcad17cf..423ee5287 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -21,10 +21,11 @@ import { } from './auth'; import { updater } from '../updater'; import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages'; -import downloadMediaWithClient from './media'; +import downloadMediaWithClient, { parseMediaUrl } from './media'; import { buildApiUserFromFull } from '../apiBuilders/users'; import localDb from '../localDb'; import { buildApiPeerId } from '../apiBuilders/peers'; +import { addMessageToLocalDb } from '../helpers'; const DEFAULT_USER_AGENT = 'Unknown UserAgent'; const DEFAULT_PLATFORM = 'Unknown platform'; @@ -265,7 +266,21 @@ export function downloadMedia( args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean }, onProgress?: ApiOnProgress, ) { - return downloadMediaWithClient(args, client, isConnected, onProgress); + return downloadMediaWithClient(args, client, isConnected, onProgress).catch(async (err) => { + if (err.message.startsWith('FILE_REFERENCE')) { + const isFileReferenceRepaired = await repairFileReference({ url: args.url }); + if (!isFileReferenceRepaired) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.error('Failed to repair file reference', args.url); + } + return undefined; + } + + return downloadMediaWithClient(args, client, isConnected, onProgress); + } + return undefined; + }); } export function uploadFile(file: File, onProgress?: ApiOnProgress) { @@ -338,3 +353,50 @@ async function handleTerminatedSession() { } } } + +export async function repairFileReference({ + url, +}: { + url: string; +}) { + const parsed = parseMediaUrl(url); + + if (!parsed) return undefined; + + const { + entityType, entityId, mediaMatchType, + } = parsed; + + if (mediaMatchType === 'file') { + return false; + } + + if (entityType === 'msg') { + const entity = localDb.messages[entityId]!; + const messageId = entity.id; + + const peer = 'channelId' in entity.peerId ? new GramJs.InputChannel({ + channelId: entity.peerId.channelId, + accessHash: (localDb.chats[buildApiPeerId(entity.peerId.channelId, 'channel')] as GramJs.Channel).accessHash!, + }) : undefined; + const result = await invokeRequest( + peer + ? new GramJs.channels.GetMessages({ + channel: peer, + id: [new GramJs.InputMessageID({ id: messageId })], + }) + : new GramJs.messages.GetMessages({ + id: [new GramJs.InputMessageID({ id: messageId })], + }), + ); + + if (!result || result instanceof GramJs.messages.MessagesNotModified) return false; + + const message = result.messages[0]; + if (message instanceof GramJs.MessageEmpty) return false; + addMessageToLocalDb(message); + return true; + } + + return false; +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index f1224d842..2b021a711 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -1,5 +1,5 @@ export { - destroy, disconnect, downloadMedia, fetchCurrentUser, + destroy, disconnect, downloadMedia, fetchCurrentUser, repairFileReference, } from './client'; export { diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index a68cf746b..128734db3 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -11,13 +11,8 @@ import { MEDIA_CACHE_NAME_AVATARS, } from '../../../config'; import localDb from '../localDb'; -import { getEntityTypeById } from '../gramjsBuilders'; import * as cacheApi from '../../../util/cacheApi'; - -type EntityType = ( - 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' | - 'document' -); +import { getEntityTypeById } from '../gramjsBuilders'; const MEDIA_ENTITY_TYPES = new Set(['msg', 'sticker', 'gif', 'wallpaper', 'photo', 'webDocument', 'document']); const TGS_MIME_TYPE = 'application/x-tgsticker'; @@ -64,6 +59,11 @@ export default async function downloadMedia( }; } +export type EntityType = ( + 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'channel' | 'chat' | 'user' | 'photo' | 'stickerSet' | 'webDocument' | + 'document' + ); + async function download( url: string, client: TelegramClient, @@ -74,19 +74,16 @@ async function download( mediaFormat?: ApiMediaFormat, isHtmlAllowed?: boolean, ) { - const mediaMatch = url.startsWith('staticMap') - ? url.match(/(staticMap):([0-9-]+)(\?.+)/) - : url.startsWith('webDocument') - ? url.match(/(webDocument):(.+)/) - : url.match( - /(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/, - ); - if (!mediaMatch) { - return undefined; - } + const parsed = parseMediaUrl(url); - if (mediaMatch[1] === 'file') { - const response = await fetch(mediaMatch[2]); + if (!parsed) return undefined; + + const { + entityType, entityId, sizeType, params, mediaMatchType, + } = parsed; + + if (entityType === 'file') { + const response = await fetch(entityId); const data = await response.arrayBuffer(); return { data }; } @@ -95,18 +92,8 @@ async function download( return Promise.reject(new Error('ERROR: Client is not connected')); } - let entityType: EntityType; - const entityId: string | number = mediaMatch[2]; - const sizeType = mediaMatch[3] ? mediaMatch[3].replace('?size=', '') : undefined; - let entity: ( - GramJs.User | GramJs.Chat | GramJs.Channel | GramJs.Photo | - GramJs.Message | GramJs.MessageService | - GramJs.Document | GramJs.StickerSet | GramJs.TypeWebDocument | undefined - ); - - if (mediaMatch[1] === 'staticMap') { - const accessHash = mediaMatch[2]; - const params = mediaMatch[3]; + if (entityType === 'staticMap') { + const accessHash = entityId; const parsedParams = new URLSearchParams(params); const long = parsedParams.get('long'); const lat = parsedParams.get('lat'); @@ -123,13 +110,11 @@ async function download( }; } - if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') { - entityType = getEntityTypeById(entityId); - } else { - entityType = mediaMatch[1] as ( - 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo' | 'webDocument' | 'document' - ); - } + let entity: ( + GramJs.User | GramJs.Chat | GramJs.Channel | GramJs.Photo | + GramJs.Message | GramJs.MessageService | + GramJs.Document | GramJs.StickerSet | GramJs.TypeWebDocument | undefined + ); switch (entityType) { case 'channel': @@ -209,7 +194,7 @@ async function download( return { mimeType, data }; } else { - const data = await client.downloadProfilePhoto(entity, mediaMatch[1] === 'profile'); + const data = await client.downloadProfilePhoto(entity, mediaMatchType === 'profile'); const mimeType = getMimeType(data); return { mimeType, data }; @@ -310,3 +295,47 @@ function getMimeType(data: Uint8Array, fallbackMimeType = 'image/jpeg') { return type; } + +export function parseMediaUrl(url: string) { + const mediaMatch = url.startsWith('staticMap') + ? url.match(/(staticMap):([0-9-]+)(\?.+)/) + : url.startsWith('webDocument') + ? url.match(/(webDocument):(.+)/) + : url.match( + /(avatar|profile|photo|msg|stickerSet|sticker|wallpaper|gif|file|document)([-\d\w./]+)(?::\d+)?(\?size=\w+)?/, + ); + if (!mediaMatch) { + return undefined; + } + + const mediaMatchType = mediaMatch[1]; + const entityId: string | number = mediaMatch[2]; + + if (mediaMatchType === 'file') { + return { + mediaMatchType, + entityType: 'file', + entityId, + }; + } + + let entityType: EntityType; + const params = mediaMatch[3]; + const sizeType = params?.replace('?size=', '') || undefined; + + if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') { + entityType = getEntityTypeById(entityId); + } else { + entityType = mediaMatch[1] as ( + 'msg' | 'sticker' | 'wallpaper' | 'gif' | 'stickerSet' | 'photo' | 'webDocument' | 'document' + ); + } + + return { + mediaMatchType, + entityType, + entityId, + sizeType, + params, + }; +}