/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE * * Originally from: * https://github.com/zhukov/webogram * Copyright (C) 2014 Igor Zhukov * https://github.com/zhukov/webogram/blob/master/LICENSE */ import {AccountWallPapers, Document, DocumentAttribute, MessagesSavedGifs, PhotoSize, WallPaper} from '../../layer'; import {ReferenceContext} from '../mtproto/referenceDatabase'; import {getFullDate} from '../../helpers/date'; import isObject from '../../helpers/object/isObject'; import safeReplaceArrayInObject from '../../helpers/object/safeReplaceArrayInObject'; import {AppManager} from './manager'; import wrapPlainText from '../richTextProcessor/wrapPlainText'; import assumeType from '../../helpers/assumeType'; import {getEnvironment} from '../../environment/utils'; import {isServiceWorkerOnline} from '../mtproto/mtproto.worker'; import MTProtoMessagePort from '../mtproto/mtprotoMessagePort'; import getDocumentInput from './utils/docs/getDocumentInput'; import getDocumentURL from './utils/docs/getDocumentURL'; import type {ThumbCache} from '../storages/thumbs'; import makeError from '../../helpers/makeError'; export type MyDocument = Document.document; // TODO: если залить картинку файлом, а потом перезайти в диалог - превьюшка заново скачается const EXTENSION_MIME_TYPE_MAP = { mov: 'video/quicktime', gif: 'image/gif', pdf: 'application/pdf' }; type WallPaperId = WallPaper.wallPaper['id']; let uploadWallPaperTempId = 0; export class AppDocsManager extends AppManager { private docs: {[docId: DocId]: MyDocument}; private stickerCachedThumbs: {[docId: DocId]: {[toneIndex: number]: {url: string, w: number, h: number}}}; private uploadingWallPapers: {[id: WallPaperId]: {cacheContext: ThumbCache, file: File}}; protected after() { this.docs = {}; this.stickerCachedThumbs = {}; this.uploadingWallPapers = {}; MTProtoMessagePort.getInstance().addEventListener('serviceWorkerOnline', (online) => { if(!online) { this.onServiceWorkerFail(); } }); } private onServiceWorkerFail = () => { for(const id in this.docs) { const doc = this.docs[id]; if(doc.supportsStreaming) { delete doc.supportsStreaming; this.thumbsStorage.deleteCacheContext(doc); } } }; public saveDoc(doc: Document, context?: ReferenceContext): MyDocument { if(!doc || doc._ === 'documentEmpty') { return; } const oldDoc = this.docs[doc.id]; if(doc.file_reference) { // * because we can have a new object w/o the file_reference while sending safeReplaceArrayInObject('file_reference', oldDoc, doc); this.referenceDatabase.saveContext(doc.file_reference, context); } // console.log('saveDoc', apiDoc, this.docs[apiDoc.id]); // if(oldDoc) { // //if(doc._ !== 'documentEmpty' && doc._ === d._) { // if(doc.thumbs) { // if(!oldDoc.thumbs) oldDoc.thumbs = doc.thumbs; // /* else if(apiDoc.thumbs[0].bytes && !d.thumbs[0].bytes) { // d.thumbs.unshift(apiDoc.thumbs[0]); // } else if(d.thumbs[0].url) { // fix for converted thumb in safari // apiDoc.thumbs[0] = d.thumbs[0]; // } */ // } // //} // return oldDoc; // //return Object.assign(d, apiDoc, context); // //return context ? Object.assign(d, context) : d; // } if(!oldDoc) { this.docs[doc.id] = doc; } // * exclude from state // defineNotNumerableProperties(doc, [/* 'thumbs', */'type', 'h', 'w', 'file_name', // 'file', 'duration', 'downloaded', 'url', 'audioTitle', // 'audioPerformer', 'sticker', 'stickerEmoji', 'stickerEmojiRaw', // 'stickerSetInput', 'stickerThumbConverted', 'animated', 'supportsStreaming']); for(let i = 0, length = doc.attributes.length; i < length; ++i) { const attribute = doc.attributes[i]; switch(attribute._) { case 'documentAttributeFilename': doc.file_name = wrapPlainText(attribute.file_name); break; case 'documentAttributeAudio': doc.duration = attribute.duration; doc.type = attribute.pFlags.voice && doc.mime_type === 'audio/ogg' ? 'voice' : 'audio'; /* if(apiDoc.type === 'audio') { apiDoc.supportsStreaming = true; } */ break; case 'documentAttributeVideo': doc.duration = attribute.duration; doc.w = attribute.w; doc.h = attribute.h; // apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming/* && apiDoc.size > 524288 */; if(/* apiDoc.thumbs && */attribute.pFlags.round_message) { doc.type = 'round'; } else /* if(apiDoc.thumbs) */ { doc.type = 'video'; } break; case 'documentAttributeSticker': if(attribute.alt !== undefined) { doc.stickerEmojiRaw = attribute.alt; } if(attribute.stickerset) { if(attribute.stickerset._ === 'inputStickerSetEmpty') { delete attribute.stickerset; } else if(attribute.stickerset._ === 'inputStickerSetID') { doc.stickerSetInput = attribute.stickerset; } } // * there can be no thumbs, then it is a document if(/* apiDoc.thumbs && */doc.mime_type === 'image/webp' && (doc.thumbs || getEnvironment().IS_WEBP_SUPPORTED)) { doc.type = 'sticker'; doc.sticker = 1; } else if(doc.mime_type === 'video/webm') { if(!getEnvironment().IS_WEBM_SUPPORTED) { return; } doc.type = 'sticker'; doc.sticker = 3; doc.animated = true; } break; case 'documentAttributeImageSize': doc.type = 'photo'; doc.w = attribute.w; doc.h = attribute.h; break; case 'documentAttributeAnimated': if((doc.mime_type === 'image/gif' || doc.mime_type === 'video/mp4')/* && apiDoc.thumbs */) { doc.type = 'gif'; } doc.animated = true; break; } } if(!doc.mime_type) { const ext = (doc.file_name || '').split('.').pop(); // @ts-ignore const mappedMimeType = ext && EXTENSION_MIME_TYPE_MAP[ext.toLowerCase()]; if(mappedMimeType) { doc.mime_type = mappedMimeType; } else { switch(doc.type) { case 'gif': case 'video': case 'round': doc.mime_type = 'video/mp4'; break; case 'sticker': doc.mime_type = 'image/webp'; break; case 'audio': doc.mime_type = 'audio/mpeg'; break; case 'voice': doc.mime_type = 'audio/ogg'; break; default: doc.mime_type = 'application/octet-stream'; break; } } } else if(doc.mime_type === EXTENSION_MIME_TYPE_MAP.pdf) { doc.type = 'pdf'; } else if(doc.mime_type === EXTENSION_MIME_TYPE_MAP.gif) { doc.type = 'gif'; } if(doc.type === 'voice' || doc.type === 'round') { // browser will identify extension const attribute = doc.attributes.find((attribute) => attribute._ === 'documentAttributeFilename') as DocumentAttribute.documentAttributeFilename; const ext = attribute && attribute.file_name.split('.').pop(); const date = getFullDate(new Date(doc.date * 1000), {monthAsNumber: true, leadingZero: true}).replace(/[:\.]/g, '-').replace(', ', '_'); doc.file_name = `${doc.type}_${date}${ext ? '.' + ext : ''}`; } if(isServiceWorkerOnline()) { if((doc.type === 'gif' && doc.size > 8e6) || doc.type === 'audio' || doc.type === 'video'/* || doc.mime_type.indexOf('video/') === 0 */) { doc.supportsStreaming = true; const cacheContext = this.thumbsStorage.getCacheContext(doc); if(!cacheContext.url) { this.thumbsStorage.setCacheContextURL(doc, undefined, getDocumentURL(doc), 0); } } } // for testing purposes // doc.supportsStreaming = false; // doc.url = ''; // * this will break upload urls if(!doc.file_name) { doc.file_name = ''; } if(doc.mime_type === 'application/x-tgsticker' && doc.file_name === 'AnimatedSticker.tgs') { doc.type = 'sticker'; doc.animated = true; doc.sticker = 2; } /* if(!doc.url) { doc.url = this.getFileURL(doc); } */ if(oldDoc) { return Object.assign(oldDoc, doc); } return doc; } public getDoc(docId: DocId | MyDocument): MyDocument { return isObject(docId) ? docId : this.docs[docId]; } public downloadDoc(doc: MyDocument, queueId?: number, onlyCache?: boolean) { return this.apiFileManager.downloadMedia({ media: doc, queueId, onlyCache }); } public getLottieCachedThumb(docId: DocId, toneIndex: number) { const cached = this.stickerCachedThumbs[docId]; return cached && cached[toneIndex]; } public saveLottiePreview(docId: DocId, blob: Blob, width: number, height: number, toneIndex: number) { const doc = this.getDoc(docId); if(!doc) { return; } const cached = this.stickerCachedThumbs[doc.id] ??= {}; const thumb = cached[toneIndex]; if(thumb && thumb.w >= width && thumb.h >= height) { return; } cached[toneIndex] = { url: URL.createObjectURL(blob), w: width, h: height }; } public saveWebPConvertedStrippedThumb(docId: DocId, bytes: Uint8Array) { const doc = this.getDoc(docId); if(!doc) { return; } const thumb = doc.thumbs && doc.thumbs.find((thumb) => thumb._ === 'photoStrippedSize') as PhotoSize.photoStrippedSize; if(!thumb) { return; } doc.pFlags.stickerThumbConverted = true; thumb.bytes = bytes; } public getWallPapers() { return this.apiManager.invokeApiHashable({method: 'account.getWallPapers'}).then((accountWallpapers) => { const wallPapers = (accountWallpapers as AccountWallPapers.accountWallPapers).wallpapers as WallPaper.wallPaper[]; wallPapers.forEach((wallPaper) => { wallPaper.document = this.saveDoc(wallPaper.document); }); return wallPapers; }); } public prepareWallPaperUpload(file: File) { const id = 'wallpaper-upload-' + ++uploadWallPaperTempId; const thumb = { _: 'photoSize', h: 0, w: 0, location: {} as any, size: file.size, type: 'full' } as PhotoSize.photoSize; let document: MyDocument = { _: 'document', access_hash: '', attributes: [], dc_id: 0, file_reference: [], id, mime_type: file.type, size: file.size, date: Date.now() / 1000, pFlags: {}, thumbs: [thumb], file_name: file.name }; document = this.saveDoc(document); const cacheContext = this.thumbsStorage.setCacheContextURL(document, undefined, URL.createObjectURL(file), file.size); const wallpaper: WallPaper.wallPaper = { _: 'wallPaper', access_hash: '', document: document, id, slug: id, pFlags: {} }; this.uploadingWallPapers[id] = { cacheContext, file }; return wallpaper; } public uploadWallPaper(id: WallPaperId) { const {cacheContext, file} = this.uploadingWallPapers[id]; delete this.uploadingWallPapers[id]; const upload = this.apiFileManager.upload({file, fileName: file.name}); return upload.then((inputFile) => { return this.apiManager.invokeApi('account.uploadWallPaper', { file: inputFile, mime_type: file.type, settings: { _: 'wallPaperSettings' } }).then((wallPaper) => { assumeType(wallPaper); wallPaper.document = this.saveDoc(wallPaper.document); this.thumbsStorage.setCacheContextURL(wallPaper.document, undefined, cacheContext.url, cacheContext.downloaded); return wallPaper; }); }); } public getGifs() { return this.apiManager.invokeApiHashable({ method: 'messages.getSavedGifs', processResult: (res) => { assumeType(res); return res.gifs.map((doc) => this.saveDoc(doc)); } }); } public requestDocPart(docId: DocId, dcId: number, offset: number, limit: number) { const doc = this.getDoc(docId); if(!doc) return Promise.reject(makeError('NO_DOC')); return this.apiFileManager.requestFilePart(dcId, getDocumentInput(doc), offset, limit); } }