/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import type { RequestFilePartTask, RequestFilePartTaskResponse, ServiceWorkerTask } from '../serviceWorker/index.service'; import type { Awaited, WorkerTaskVoidTemplate } from '../../types'; import type { CacheStorageDbName } from '../cacheStorage'; import type { State } from '../../config/state'; import type { Message, MessagePeerReaction, PeerNotifySettings } from '../../layer'; import { CryptoMethods } from '../crypto/crypto_methods'; import rootScope from '../rootScope'; import webpWorkerController from '../webp/webpWorkerController'; import { MOUNT_CLASS_TO } from '../../config/debug'; import sessionStorage from '../sessionStorage'; import webPushApiManager from './webPushApiManager'; import appRuntimeManager from '../appManagers/appRuntimeManager'; import telegramMeWebManager from './telegramMeWebManager'; import pause from '../../helpers/schedulers/pause'; import isObject from '../../helpers/object/isObject'; import ENVIRONMENT from '../../environment'; import loadState from '../appManagers/utils/state/loadState'; import opusDecodeController from '../opusDecodeController'; import MTProtoMessagePort from './mtprotoMessagePort'; import cryptoMessagePort from '../crypto/cryptoMessagePort'; import SuperMessagePort from './superMessagePort'; import IS_SHARED_WORKER_SUPPORTED from '../../environment/sharedWorkerSupport'; import toggleStorages from '../../helpers/toggleStorages'; import idleController from '../../helpers/idleController'; export interface ToggleStorageTask extends WorkerTaskVoidTemplate { type: 'toggleStorages', payload: {enabled: boolean, clearWrite: boolean} }; export type Mirrors = { state: State }; export type MirrorTaskPayload = { name: T, key?: K, value: any }; export type NotificationBuildTaskPayload = { message: Message.message | Message.messageService, fwdCount?: number, peerReaction?: MessagePeerReaction, peerTypeNotifySettings?: PeerNotifySettings }; export type TabState = { chatPeerIds: PeerId[], idleStartTime: number, }; class ApiManagerProxy extends MTProtoMessagePort { private worker: /* Window */Worker; private isSWRegistered: boolean; // private sockets: Map = new Map(); private taskListenersSW: {[taskType: string]: (task: any) => void}; private mirrors: Mirrors; public newVersion: string; public oldVersion: string; private tabState: TabState; constructor() { super(); this.isSWRegistered = true; this.taskListenersSW = {}; this.mirrors = {} as any; this.tabState = { chatPeerIds: [], idleStartTime: 0 }; this.log('constructor'); /// #if !MTPROTO_SW this.registerWorker(); /// #endif this.registerServiceWorker(); this.registerCryptoWorker(); this.addMultipleEventsListeners({ convertWebp: ({fileName, bytes}) => { return webpWorkerController.convert(fileName, bytes); }, convertOpus: ({fileName, bytes}) => { return opusDecodeController.pushDecodeTask(bytes, false).then((result) => result.bytes); }, event: ({name, args}) => { // @ts-ignore rootScope.dispatchEventSingle(name, ...args); }, localStorageProxy: (payload) => { const storageTask = payload; return (sessionStorage[storageTask.type] as any)(...storageTask.args); }, mirror: this.onMirrorTask }); // this.addTaskListener('socketProxy', (task) => { // const socketTask = task.payload; // const id = socketTask.id; // //console.log('socketProxy', socketTask, id); // if(socketTask.type === 'send') { // const socket = this.sockets.get(id); // socket.send(socketTask.payload); // } else if(socketTask.type === 'close') { // will remove from map in onClose // const socket = this.sockets.get(id); // socket.close(); // } else if(socketTask.type === 'setup') { // const socket = new Socket(socketTask.payload.dcId, socketTask.payload.url, socketTask.payload.logSuffix); // const onOpen = () => { // //console.log('socketProxy onOpen'); // this.postMessage({ // type: 'socketProxy', // payload: { // type: 'open', // id // } // }); // }; // const onClose = () => { // this.postMessage({ // type: 'socketProxy', // payload: { // type: 'close', // id // } // }); // socket.removeEventListener('open', onOpen); // socket.removeEventListener('close', onClose); // socket.removeEventListener('message', onMessage); // this.sockets.delete(id); // }; // const onMessage = (buffer: ArrayBuffer) => { // this.postMessage({ // type: 'socketProxy', // payload: { // type: 'message', // id, // payload: buffer // } // }); // }; // socket.addEventListener('open', onOpen); // socket.addEventListener('close', onClose); // socket.addEventListener('message', onMessage); // this.sockets.set(id, socket); // } // }); rootScope.addEventListener('language_change', (language) => { rootScope.managers.networkerFactory.setLanguage(language); }); window.addEventListener('online', () => { rootScope.managers.networkerFactory.forceReconnectTimeout(); }); rootScope.addEventListener('logging_out', () => { const toClear: CacheStorageDbName[] = ['cachedFiles', 'cachedStreamChunks']; Promise.all([ toggleStorages(false, true), sessionStorage.clear(), Promise.race([ telegramMeWebManager.setAuthorized(false), pause(3000) ]), webPushApiManager.forceUnsubscribe(), Promise.all(toClear.map((cacheName) => caches.delete(cacheName))) ]).finally(() => { appRuntimeManager.reload(); }); }); idleController.addEventListener('change', (idle) => { this.updateTabStateIdle(idle); }); this.updateTabStateIdle(idleController.isIdle); this.log('Passing environment:', ENVIRONMENT); this.invoke('environment', ENVIRONMENT); // this.sendState(); } private registerServiceWorker() { if(!('serviceWorker' in navigator)) return; // ! I hate webpack - it won't load it by using worker.register, only navigator.serviceWork will do it. const worker = navigator.serviceWorker; navigator.serviceWorker.register( /* webpackChunkName: "sw" */ new URL('../serviceWorker/index.service', import.meta.url), {scope: './'} ).then((registration) => { this.log('SW registered', registration); this.isSWRegistered = true; const sw = registration.installing || registration.waiting || registration.active; sw.addEventListener('statechange', (e) => { this.log('SW statechange', e); }); //this.postSWMessage = worker.controller.postMessage.bind(worker.controller); /// #if MTPROTO_SW const controller = worker.controller || registration.installing || registration.waiting || registration.active; this.onWorkerFirstMessage(controller); /// #endif }, (err) => { this.isSWRegistered = false; this.log.error('SW registration failed!', err); this.invokeVoid('serviceWorkerOnline', false); }); worker.addEventListener('controllerchange', () => { this.log.warn('controllerchange'); worker.controller.addEventListener('error', (e) => { this.log.error('controller error:', e); }); }); /// #if MTPROTO_SW this.attachListenPort(worker); // this.s(); /// #else worker.addEventListener('message', (e) => { const task: ServiceWorkerTask = e.data; if(!isObject(task)) { return; } const callback = this.taskListenersSW[task.type]; if(callback) { callback(task); } }); this.addServiceWorkerTaskListener('requestFilePart', (task: RequestFilePartTask) => { const responseTask: RequestFilePartTaskResponse = { type: task.type, id: task.id }; const {docId, dcId, offset, limit} = task.payload; rootScope.managers.appDocsManager.requestDocPart(docId, dcId, offset, limit) .then((uploadFile) => { responseTask.payload = uploadFile; this.postSWMessage(responseTask); }, (err) => { responseTask.originalPayload = task.payload; responseTask.error = err; this.postSWMessage(responseTask); }); }); /// #endif worker.addEventListener('messageerror', (e) => { this.log.error('SW messageerror:', e); }); } private registerCryptoWorker() { let worker: SharedWorker | Worker; if(IS_SHARED_WORKER_SUPPORTED) { worker = new SharedWorker( /* webpackChunkName: "crypto.worker" */ new URL('../crypto/crypto.worker.ts', import.meta.url), {type: 'module'} ); } else { worker = new Worker( /* webpackChunkName: "crypto.worker" */ new URL('../crypto/crypto.worker.ts', import.meta.url), {type: 'module'} ); } cryptoMessagePort.addEventListener('port', (payload, source, event) => { this.invokeVoid('cryptoPort', undefined, undefined, [event.ports[0]]); }); this.attachWorkerToPort(worker, cryptoMessagePort, 'crypto'); } /// #if !MTPROTO_SW private registerWorker() { // return; let worker: SharedWorker | Worker; if(IS_SHARED_WORKER_SUPPORTED) { worker = new SharedWorker( /* webpackChunkName: "mtproto.worker" */ new URL('./mtproto.worker.ts', import.meta.url), {type: 'module'} ); } else { worker = new Worker( /* webpackChunkName: "mtproto.worker" */ new URL('./mtproto.worker.ts', import.meta.url), {type: 'module'} ); } this.onWorkerFirstMessage(worker); } /// #endif private attachWorkerToPort(worker: SharedWorker | Worker, messagePort: SuperMessagePort, type: string) { const port: MessagePort = (worker as SharedWorker).port || worker as any; messagePort.attachPort(port); worker.addEventListener('error', (err) => { this.log.error(type, 'worker error', err); }); } public postSWMessage(message: any) { if(navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage(message); } } private onWorkerFirstMessage(worker: any) { this.log('set webWorker'); this.worker = worker; /// #if MTPROTO_SW this.attachSendPort(worker); /// #else this.attachWorkerToPort(worker, this, 'mtproto'); /// #endif } public addServiceWorkerTaskListener(name: keyof ApiManagerProxy['taskListenersSW'], callback: ApiManagerProxy['taskListenersSW'][typeof name]) { this.taskListenersSW[name] = callback; } private loadState() { return Promise.all([ loadState().then((stateResult) => { this.newVersion = stateResult.newVersion; this.oldVersion = stateResult.oldVersion; this.mirrors['state'] = stateResult.state; return stateResult; }), // loadStorages(createStorages()), ]); } public sendState() { return this.loadState().then((result) => { const [stateResult] = result; this.invoke('state', {...stateResult, userId: rootScope.myId.toUserId()}); return result; }); } /// #if MTPROTO_WORKER public invokeCrypto(method: Method, ...args: Parameters): Promise>> { return cryptoMessagePort.invokeCrypto(method, ...args); } /// #endif public async toggleStorages(enabled: boolean, clearWrite: boolean) { await toggleStorages(enabled, clearWrite); this.invoke('toggleStorages', {enabled, clearWrite}); const task: ToggleStorageTask = {type: 'toggleStorages', payload: {enabled, clearWrite}}; this.postSWMessage(task); } public async getMirror(name: T) { const mirror = this.mirrors[name]; return mirror; } public getState() { return this.getMirror('state'); } public updateTabState(key: T, value: TabState[T]) { this.tabState[key] = value; this.invokeVoid('tabState', this.tabState); } public updateTabStateIdle(idle: boolean) { this.updateTabState('idleStartTime', idle ? Date.now() : 0); } private onMirrorTask = (payload: MirrorTaskPayload) => { const {name, key, value} = payload; if(!payload.hasOwnProperty('key')) { this.mirrors[name] = value; return; } const mirror = this.mirrors[name] ??= {} as any; if(value === undefined) { delete mirror[key]; } else { mirror[key] = value; } }; } interface ApiManagerProxy extends MTProtoMessagePort {} const apiManagerProxy = new ApiManagerProxy(); MOUNT_CLASS_TO.apiManagerProxy = apiManagerProxy; export default apiManagerProxy;