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.
496 lines
16 KiB
496 lines
16 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import type {Awaited} from '../../types'; |
|
import type {CacheStorageDbName} from '../files/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 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'; |
|
import ServiceMessagePort from '../serviceWorker/serviceMessagePort'; |
|
import App from '../../config/app'; |
|
import deferredPromise, {CancellablePromise} from '../../helpers/cancellablePromise'; |
|
|
|
export type Mirrors = { |
|
state: State |
|
}; |
|
|
|
export type MirrorTaskPayload<T extends keyof Mirrors = keyof Mirrors, K extends keyof Mirrors[T] = keyof Mirrors[T], J extends Mirrors[T][K] = Mirrors[T][K]> = { |
|
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 sockets: Map<number, Socket> = new Map(); |
|
private mirrors: Mirrors; |
|
|
|
public newVersion: string; |
|
public oldVersion: string; |
|
|
|
private tabState: TabState; |
|
|
|
public serviceMessagePort: ServiceMessagePort<true>; |
|
private lastServiceWorker: ServiceWorker; |
|
|
|
private pingServiceWorkerPromise: CancellablePromise<void>; |
|
|
|
constructor() { |
|
super(); |
|
|
|
this.mirrors = {} as any; |
|
this.tabState = { |
|
chatPeerIds: [], |
|
idleStartTime: 0 |
|
}; |
|
|
|
this.log('constructor'); |
|
|
|
// #if !MTPROTO_SW |
|
this.registerWorker(); |
|
// #endif |
|
|
|
this.registerServiceWorker(); |
|
this.registerCryptoWorker(); |
|
|
|
// const perf = performance.now(); |
|
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 |
|
|
|
// hello: () => { |
|
// this.log.error('time hello', performance.now() - perf); |
|
// } |
|
}); |
|
|
|
// 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(); |
|
} |
|
|
|
public pingServiceWorkerWithIframe() { |
|
if(this.pingServiceWorkerPromise) { |
|
return this.pingServiceWorkerPromise; |
|
} |
|
|
|
const promise = this.pingServiceWorkerPromise = deferredPromise<void>(); |
|
const iframe = document.createElement('iframe'); |
|
iframe.hidden = true; |
|
const onLoad = () => { |
|
setTimeout(() => { // ping once in 10 seconds |
|
this.pingServiceWorkerPromise = undefined; |
|
}, 10e3); |
|
|
|
clearTimeout(timeout); |
|
iframe.remove(); |
|
iframe.removeEventListener('load', onLoad); |
|
iframe.removeEventListener('error', onLoad); |
|
promise.resolve(); |
|
}; |
|
iframe.addEventListener('load', onLoad); |
|
iframe.addEventListener('error', onLoad); |
|
iframe.src = 'ping/' + (Math.random() * 0xFFFFFFFF | 0); |
|
document.body.append(iframe); |
|
|
|
const timeout = window.setTimeout(onLoad, 1e3); |
|
return promise; |
|
} |
|
|
|
private attachServiceWorker(serviceWorker: ServiceWorker) { |
|
this.lastServiceWorker && this.serviceMessagePort.detachPort(this.lastServiceWorker); |
|
this.serviceMessagePort.attachSendPort(this.lastServiceWorker = serviceWorker); |
|
this.serviceMessagePort.invokeVoid('hello', undefined); |
|
} |
|
|
|
private _registerServiceWorker() { |
|
navigator.serviceWorker.register( |
|
/* webpackChunkName: "sw" */ |
|
new URL('../serviceWorker/index.service', import.meta.url), |
|
{scope: './'} |
|
).then((registration) => { |
|
this.log('SW registered', registration); |
|
|
|
// ! doubtful fix for hard refresh |
|
if(registration.active && !navigator.serviceWorker.controller) { |
|
return registration.unregister().then(() => { |
|
window.location.reload(); |
|
}); |
|
} |
|
|
|
const sw = registration.installing || registration.waiting || registration.active; |
|
sw.addEventListener('statechange', (e) => { |
|
this.log('SW statechange', e); |
|
}); |
|
|
|
const controller = navigator.serviceWorker.controller || registration.installing || registration.waiting || registration.active; |
|
this.attachServiceWorker(controller); |
|
|
|
// #if MTPROTO_SW |
|
this.onWorkerFirstMessage(controller); |
|
// #endif |
|
}, (err) => { |
|
this.log.error('SW registration failed!', err); |
|
|
|
this.invokeVoid('serviceWorkerOnline', false); |
|
}); |
|
} |
|
|
|
private registerServiceWorker() { |
|
if(!('serviceWorker' in navigator)) return; |
|
|
|
this.serviceMessagePort = new ServiceMessagePort<true>(); |
|
|
|
// this.addMultipleEventsListeners({ |
|
// hello: () => { |
|
// // this.serviceMessagePort.invokeVoid('port', undefined); |
|
// } |
|
// }); |
|
|
|
// ! I hate webpack - it won't load it by using worker.register, only navigator.serviceWorker will do it. |
|
const worker = navigator.serviceWorker; |
|
this._registerServiceWorker(); |
|
|
|
// worker.startMessages(); |
|
|
|
worker.addEventListener('controllerchange', () => { |
|
this.log.warn('controllerchange'); |
|
|
|
const controller = worker.controller; |
|
this.attachServiceWorker(controller); |
|
|
|
controller.addEventListener('error', (e) => { |
|
this.log.error('controller error:', e); |
|
}); |
|
}); |
|
|
|
// #if MTPROTO_SW |
|
this.attachListenPort(worker); |
|
// #else |
|
this.serviceMessagePort.attachListenPort(worker); |
|
this.serviceMessagePort.addMultipleEventsListeners({ |
|
port: (payload, source, event) => { |
|
this.invokeVoid('serviceWorkerPort', undefined, undefined, [event.ports[0]]); |
|
} |
|
}); |
|
// #endif |
|
|
|
worker.addEventListener('messageerror', (e) => { |
|
this.log.error('SW messageerror:', e); |
|
}); |
|
} |
|
|
|
private async registerCryptoWorker() { |
|
const get = (url: string) => { |
|
return fetch(url).then((response) => response.text()).then((text) => { |
|
const pathnameSplitted = location.pathname.split('/'); |
|
pathnameSplitted[pathnameSplitted.length - 1] = ''; |
|
const pre = location.origin + pathnameSplitted.join('/'); |
|
text = ` |
|
var originalImportScripts = importScripts; |
|
importScripts = (url) => { |
|
console.log('importScripts', url); |
|
var newUrl = '${pre}' + url.split('/').pop(); |
|
return originalImportScripts(newUrl); |
|
}; |
|
${text}`; |
|
const blob = new Blob([text], {type: 'application/javascript'}); |
|
return blob; |
|
}); |
|
}; |
|
|
|
const workerHandler = { |
|
construct(target: any, args: any): any { |
|
const url = args[0] + location.search; |
|
return {url}; |
|
} |
|
}; |
|
|
|
const originals = [ |
|
Worker, |
|
typeof(SharedWorker) !== 'undefined' && SharedWorker |
|
].filter(Boolean); |
|
originals.forEach((w) => window[w.name as any] = new Proxy(w, workerHandler)); |
|
|
|
const worker: SharedWorker | Worker = new Worker( |
|
/* webpackChunkName: "crypto.worker" */ |
|
new URL('../crypto/crypto.worker.ts', import.meta.url), |
|
{type: 'module'} |
|
); |
|
|
|
originals.forEach((w) => window[w.name as any] = w as any); |
|
|
|
const blob = await get((worker as any).url); |
|
const urlsPromise = await this.invoke('createProxyWorkerURLs', blob); |
|
const workers = urlsPromise.map((url) => { |
|
return new (IS_SHARED_WORKER_SUPPORTED ? SharedWorker : Worker)(url, {type: 'module'}); |
|
}); |
|
|
|
// let cryptoWorkers = workers.length; |
|
cryptoMessagePort.addEventListener('port', (payload, source, event) => { |
|
this.invokeVoid('cryptoPort', undefined, undefined, [event.ports[0]]); |
|
// .then((attached) => { |
|
// if(!attached && cryptoWorkers-- > 1) { |
|
// this.log.error('terminating unneeded crypto worker'); |
|
|
|
// cryptoMessagePort.invokeVoid('terminate', undefined, source); |
|
// const worker = workers.find((worker) => (worker as SharedWorker).port === source || (worker as any) === source); |
|
// if((worker as SharedWorker).port) (worker as SharedWorker).port.close(); |
|
// else (worker as Worker).terminate(); |
|
// cryptoMessagePort.detachPort(source); |
|
// } |
|
// }); |
|
}); |
|
|
|
workers.forEach((worker) => { |
|
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<any, any, any>, 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); |
|
}); |
|
} |
|
|
|
private onWorkerFirstMessage(worker: any) { |
|
this.log('set webWorker'); |
|
|
|
// this.worker = worker; |
|
// #if MTPROTO_SW |
|
this.attachSendPort(worker); |
|
// #else |
|
this.attachWorkerToPort(worker, this, 'mtproto'); |
|
// #endif |
|
} |
|
|
|
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 extends keyof CryptoMethods>(method: Method, ...args: Parameters<CryptoMethods[typeof method]>): Promise<Awaited<ReturnType<CryptoMethods[typeof method]>>> { |
|
return cryptoMessagePort.invokeCrypto(method, ...args); |
|
} |
|
// #endif |
|
|
|
public async toggleStorages(enabled: boolean, clearWrite: boolean) { |
|
await toggleStorages(enabled, clearWrite); |
|
this.invoke('toggleStorages', {enabled, clearWrite}); |
|
this.serviceMessagePort.invokeVoid('toggleStorages', {enabled, clearWrite}); |
|
} |
|
|
|
public async getMirror<T extends keyof Mirrors>(name: T) { |
|
const mirror = this.mirrors[name]; |
|
return mirror; |
|
} |
|
|
|
public getState() { |
|
return this.getMirror('state'); |
|
} |
|
|
|
public updateTabState<T extends keyof TabState>(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<true> {} |
|
|
|
const apiManagerProxy = new ApiManagerProxy(); |
|
MOUNT_CLASS_TO.apiManagerProxy = apiManagerProxy; |
|
export default apiManagerProxy;
|
|
|