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

/*
* 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;