Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

476 lines
16 KiB

/*
* 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 <igor.beatle@gmail.com>
* https://github.com/zhukov/webogram/blob/master/LICENSE
*/
import { FileURLType, getFileNameByLocation, getFileURL } from '../../helpers/fileName';
3 years ago
import { Document, InputFileLocation, InputMedia, PhotoSize } from '../../layer';
import referenceDatabase, { ReferenceContext } from '../mtproto/referenceDatabase';
import opusDecodeController from '../opusDecodeController';
import { RichTextProcessor } from '../richtextprocessor';
import appDownloadManager, { DownloadBlob } from './appDownloadManager';
import appPhotosManager from './appPhotosManager';
import blur from '../../helpers/blur';
import apiManager from '../mtproto/mtprotoworker';
4 years ago
import { MOUNT_CLASS_TO } from '../../config/debug';
import { getFullDate } from '../../helpers/date';
import rootScope from '../rootScope';
import IS_WEBP_SUPPORTED from '../../environment/webpSupport';
import IS_WEBM_SUPPORTED from '../../environment/webmSupport';
import defineNotNumerableProperties from '../../helpers/object/defineNotNumerableProperties';
import isObject from '../../helpers/object/isObject';
import safeReplaceArrayInObject from '../../helpers/object/safeReplaceArrayInObject';
export type MyDocument = Document.document;
5 years ago
// TODO: если залить картинку файлом, а потом перезайти в диалог - превьюшка заново скачается
const EXTENSION_MIME_TYPE_MAP = {
mov: 'video/quicktime',
gif: 'image/gif',
pdf: 'application/pdf',
};
export class AppDocsManager {
3 years ago
private docs: {[docId: DocId]: MyDocument} = {};
private savingLottiePreview: {[docId: DocId]: true} = {};
public downloading: Map<DocId, DownloadBlob> = new Map();
constructor() {
apiManager.onServiceWorkerFail = this.onServiceWorkerFail;
}
public onServiceWorkerFail = () => {
for(const id in this.docs) {
const doc = this.docs[id];
if(doc.supportsStreaming) {
delete doc.supportsStreaming;
const cacheContext = appDownloadManager.getCacheContext(doc);
delete cacheContext.url;
}
}
};
public saveDoc(doc: Document, context?: ReferenceContext): MyDocument {
if(doc._ === 'documentEmpty') {
return undefined;
}
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);
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];
5 years ago
switch(attribute._) {
case 'documentAttributeFilename':
doc.file_name = RichTextProcessor.wrapPlainText(attribute.file_name);
doc.fileName = RichTextProcessor.wrapEmojiText(attribute.file_name);
5 years ago
break;
case 'documentAttributeAudio':
doc.duration = attribute.duration;
doc.audioTitle = RichTextProcessor.wrapEmojiText(attribute.title);
doc.audioPerformer = RichTextProcessor.wrapEmojiText(attribute.performer);
doc.type = attribute.pFlags.voice && doc.mime_type === 'audio/ogg' ? 'voice' : 'audio';
/* if(apiDoc.type === 'audio') {
apiDoc.supportsStreaming = true;
} */
5 years ago
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';
5 years ago
}
break;
case 'documentAttributeSticker':
if(attribute.alt !== undefined) {
doc.stickerEmojiRaw = attribute.alt;
doc.stickerEmoji = RichTextProcessor.wrapRichText(doc.stickerEmojiRaw, {noLinks: true, noLinebreaks: true});
5 years ago
}
if(attribute.stickerset) {
if(attribute.stickerset._ === 'inputStickerSetEmpty') {
delete attribute.stickerset;
} else if(attribute.stickerset._ === 'inputStickerSetID') {
doc.stickerSetInput = attribute.stickerset;
5 years ago
}
}
// * there can be no thumbs, then it is a document
if(/* apiDoc.thumbs && */doc.mime_type === 'image/webp' && (doc.thumbs || IS_WEBP_SUPPORTED)) {
doc.type = 'sticker';
doc.sticker = 1;
} else if(doc.mime_type === 'video/webm') {
if(!IS_WEBM_SUPPORTED) {
return;
}
doc.type = 'sticker';
doc.sticker = 3;
doc.animated = true;
5 years ago
}
break;
case 'documentAttributeImageSize':
doc.type = 'photo';
doc.w = attribute.w;
doc.h = attribute.h;
5 years ago
break;
case 'documentAttributeAnimated':
if((doc.mime_type === 'image/gif' || doc.mime_type === 'video/mp4')/* && apiDoc.thumbs */) {
doc.type = 'gif';
5 years ago
}
doc.animated = true;
5 years ago
break;
}
}
5 years ago
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;
}
5 years ago
}
} 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
doc.file_name = doc.fileName = doc.type + '_' + getFullDate(new Date(doc.date * 1000), {monthAsNumber: true, leadingZero: true}).replace(/[:\.]/g, '-').replace(', ', '_');
}
if(apiManager.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 = appDownloadManager.getCacheContext(doc);
if(!cacheContext.url) {
cacheContext.url = this.getFileURL(doc);
}
}
}
// for testing purposes
4 years ago
// doc.supportsStreaming = false;
// doc.url = ''; // * this will break upload urls
5 years ago
if(!doc.file_name) {
doc.file_name = doc.fileName = '';
5 years ago
}
if(doc.mime_type === 'application/x-tgsticker' && doc.file_name === 'AnimatedSticker.tgs') {
doc.type = 'sticker';
doc.animated = true;
doc.sticker = 2;
}
5 years ago
/* if(!doc.url) {
doc.url = this.getFileURL(doc);
} */
if(oldDoc) {
return Object.assign(oldDoc, doc);
}
return doc;
5 years ago
}
3 years ago
public getDoc(docId: DocId | MyDocument): MyDocument {
return isObject<MyDocument>(docId) ? docId : this.docs[docId];
5 years ago
}
3 years ago
public getMediaInput(doc: MyDocument): InputMedia.inputMediaDocument {
return {
_: 'inputMediaDocument',
id: {
_: 'inputDocument',
id: doc.id,
access_hash: doc.access_hash,
file_reference: doc.file_reference
},
ttl_seconds: 0
};
}
public getInput(doc: MyDocument, thumbSize?: string): InputFileLocation.inputDocumentFileLocation {
return {
_: 'inputDocumentFileLocation',
id: doc.id,
access_hash: doc.access_hash,
file_reference: doc.file_reference,
thumb_size: thumbSize
};
}
public getFileDownloadOptions(doc: MyDocument, thumb?: PhotoSize.photoSize, queueId?: number, onlyCache?: boolean) {
const inputFileLocation = this.getInput(doc, thumb?.type);
let mimeType: string;
if(thumb) {
mimeType = doc.sticker ? 'image/webp' : 'image/jpeg'/* doc.mime_type */;
} else {
mimeType = doc.mime_type || 'application/octet-stream';
}
return {
dcId: doc.dc_id,
location: inputFileLocation,
size: thumb ? thumb.size : doc.size,
mimeType,
fileName: doc.file_name,
queueId,
onlyCache
};
}
public getFileURL(doc: MyDocument, download = false, thumb?: PhotoSize.photoSize) {
let type: FileURLType;
if(download) {
type = 'download';
} else if(thumb) {
type = 'thumb';
} else if(doc.supportsStreaming) {
type = 'stream';
} else {
type = 'document';
}
4 years ago
return getFileURL(type, this.getFileDownloadOptions(doc, thumb));
}
public getThumbURL(doc: MyDocument, thumb: PhotoSize.photoSize | PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize) {
let promise: Promise<any> = Promise.resolve();
const cacheContext = appDownloadManager.getCacheContext(doc, thumb.type);
if(!cacheContext.url) {
if('bytes' in thumb) {
promise = blur(appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker)).then(url => {
cacheContext.url = url;
}) as any;
} else {
//return this.getFileURL(doc, false, thumb);
promise = appPhotosManager.preloadPhoto(doc, thumb) as any;
}
}
return {thumb, cacheContext, promise};
}
public getThumb(doc: MyDocument, tryNotToUseBytes = true) {
const thumb = appPhotosManager.choosePhotoSize(doc, 0, 0, !tryNotToUseBytes);
if(thumb._ === 'photoSizeEmpty') return null;
return this.getThumbURL(doc, thumb as any);
}
public getInputFileName(doc: MyDocument, thumbSize?: string) {
return getFileNameByLocation(this.getInput(doc, thumbSize), {fileName: doc.file_name});
4 years ago
}
public downloadDoc(doc: MyDocument, queueId?: number, onlyCache?: boolean): DownloadBlob {
const fileName = this.getInputFileName(doc);
let download: DownloadBlob = appDownloadManager.getDownload(fileName);
if(download) {
return download;
}
const downloadOptions = this.getFileDownloadOptions(doc, undefined, queueId, onlyCache);
download = appDownloadManager.download(downloadOptions);
this.downloading.set(doc.id, download);
rootScope.dispatchEvent('download_start', doc.id);
const cacheContext = appDownloadManager.getCacheContext(doc);
const originalPromise = download;
originalPromise.then((blob) => {
cacheContext.url = URL.createObjectURL(blob);
cacheContext.downloaded = blob.size;
}, () => {}).finally(() => {
this.downloading.delete(doc.id);
});
if(doc.type === 'voice' && !opusDecodeController.isPlaySupported()) {
download = originalPromise.then(async(blob) => {
const reader = new FileReader();
4 years ago
await new Promise<void>((resolve, reject) => {
reader.onloadend = (e) => {
const uint8 = new Uint8Array(e.target.result as ArrayBuffer);
//console.log('sending uint8 to decoder:', uint8);
opusDecodeController.decode(uint8).then(result => {
cacheContext.url = result.url;
resolve();
}, (err) => {
delete cacheContext.downloaded;
reject(err);
});
};
reader.readAsArrayBuffer(blob);
});
return blob;
});
}
download.then(() => {
rootScope.dispatchEvent('document_downloaded', doc);
});
return download;
}
public isSavingLottiePreview(doc: MyDocument, toneIndex: number) {
const key = doc.id + '-' + toneIndex;
return !!this.savingLottiePreview[key];
}
public saveLottiePreview(doc: MyDocument, canvas: HTMLCanvasElement, toneIndex: number) {
const key = doc.id + '-' + toneIndex;
if(this.savingLottiePreview[key]/* || true */) return;
if(!doc.stickerCachedThumbs) {
defineNotNumerableProperties(doc, ['stickerCachedThumbs']);
doc.stickerCachedThumbs = {};
}
const thumb = doc.stickerCachedThumbs[toneIndex];
if(thumb && thumb.w >= canvas.width && thumb.h >= canvas.height) {
return;
}
/* if(doc.thumbs.find(t => t._ === 'photoStrippedSize')
|| (doc.stickerCachedThumb || (doc.stickerSavedThumbWidth >= canvas.width && doc.stickerSavedThumbHeight >= canvas.height))) {
return;
} */
this.savingLottiePreview[key] = true;
canvas.toBlob((blob) => {
//console.log('got lottie preview', doc, blob, URL.createObjectURL(blob));
const thumb = {
url: URL.createObjectURL(blob),
w: canvas.width,
h: canvas.height
};
doc.stickerCachedThumbs[toneIndex] = thumb;
delete this.savingLottiePreview[key];
/* const reader = new FileReader();
reader.onloadend = (e) => {
const uint8 = new Uint8Array(e.target.result as ArrayBuffer);
const thumb: PhotoSize.photoStrippedSize = {
_: 'photoStrippedSize',
bytes: uint8,
type: 'i'
};
doc.stickerSavedThumbWidth = canvas.width;
doc.stickerSavedThumbHeight = canvas.width;
defineNotNumerableProperties(thumb, ['url']);
thumb.url = URL.createObjectURL(blob);
doc.thumbs.findAndSplice(t => t._ === thumb._);
doc.thumbs.unshift(thumb);
if(!webpWorkerController.isWebpSupported()) {
doc.pFlags.stickerThumbConverted = true;
}
delete this.savingLottiePreview[doc.id];
};
reader.readAsArrayBuffer(blob); */
});
}
public saveDocFile(doc: MyDocument, queueId?: number) {
/* const options = this.getFileDownloadOptions(doc, undefined, queueId);
return appDownloadManager.downloadToDisc(options, doc.file_name); */
const promise = this.downloadDoc(doc, queueId);
promise.then(() => {
const cacheContext = appDownloadManager.getCacheContext(doc);
appDownloadManager.createDownloadAnchor(cacheContext.url, doc.file_name);
});
return promise;
}
5 years ago
}
4 years ago
const appDocsManager = new AppDocsManager();
MOUNT_CLASS_TO.appDocsManager = appDocsManager;
4 years ago
export default appDocsManager;