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.
429 lines
13 KiB
429 lines
13 KiB
/* |
|
* 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<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 isSWRegistered: boolean; |
|
// private sockets: Map<number, Socket> = 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<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); |
|
}); |
|
} |
|
|
|
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 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}); |
|
const task: ToggleStorageTask = {type: 'toggleStorages', payload: {enabled, clearWrite}}; |
|
this.postSWMessage(task); |
|
} |
|
|
|
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;
|
|
|