diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index c7e2f161..73c00a53 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -308,7 +308,7 @@ export default class ChatBubbles { }); this.listenerSetter.add(rootScope, 'album_edit', (e) => { - fastRaf(() => { + //fastRaf(() => { // ! can't use delayed smth here, need original bubble to be edited const {peerId, groupId, deletedMids} = e; if(peerId !== this.peerId) return; @@ -319,7 +319,7 @@ export default class ChatBubbles { const renderMaxId = getObjectKeysAndSort(this.appMessagesManager.groupedMessagesStorage[groupId], 'asc').pop(); this.renderMessage(this.chat.getMessage(renderMaxId), true, false, this.bubbles[renderedId], false); - }); + //}); }); this.listenerSetter.add(rootScope, 'messages_downloaded', (e) => { diff --git a/src/helpers/context.ts b/src/helpers/context.ts index dbb20194..09a6215e 100644 --- a/src/helpers/context.ts +++ b/src/helpers/context.ts @@ -26,7 +26,7 @@ const notifyWorker = (...args: any[]) => { (self as any as DedicatedWorkerGlobalScope).postMessage(...args); }; -const empty = () => {}; +const noop = () => {}; -export const notifySomeone = isServiceWorker ? notifyServiceWorker.bind(null, false) : (isWebWorker ? notifyWorker : empty); -export const notifyAll = isServiceWorker ? notifyServiceWorker.bind(null, true) : (isWebWorker ? notifyWorker : empty); \ No newline at end of file +export const notifySomeone = isServiceWorker ? notifyServiceWorker.bind(null, false) : (isWebWorker ? notifyWorker : noop); +export const notifyAll = isServiceWorker ? notifyServiceWorker.bind(null, true) : (isWebWorker ? notifyWorker : noop); \ No newline at end of file diff --git a/src/helpers/userAgent.ts b/src/helpers/userAgent.ts index b6a46f89..d27d40dc 100644 --- a/src/helpers/userAgent.ts +++ b/src/helpers/userAgent.ts @@ -11,7 +11,7 @@ export const isChromium = /Chrome/.test(navigator.userAgent) && /Google Inc/.tes * * This should be removed once the underlying Safari issue is fixed. */ -const ctx = typeof(window) !== 'undefined' ? window : self; +export const ctx = typeof(window) !== 'undefined' ? window : self; // https://stackoverflow.com/a/58065241 export const isAppleMobile = (/iPad|iPhone|iPod/.test(navigator.platform) || diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index c7e15976..b45d60e7 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -164,7 +164,8 @@ class ConnectionStatusComponent { DEBUG && this.log('setState: isShown:', this.connecting || this.updating); }; - this.setStateTimeout = window.setTimeout(cb, timeout); + //this.setStateTimeout = window.setTimeout(cb, timeout); + cb(); /* if(timeout) this.setStateTimeout = window.setTimeout(cb, timeout); else cb(); */ }); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index a4e66b5a..b6fe9d0a 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -176,6 +176,8 @@ export class AppMessagesManager { public dialogsStorage: DialogsStorage; public filtersStorage: FiltersStorage; + private groupedTempId = 0; + constructor() { this.dialogsStorage = new DialogsStorage(this, appChatsManager, appPeersManager, serverTimeManager); this.filtersStorage = new FiltersStorage(appPeersManager, appUsersManager, /* apiManager, */ rootScope); @@ -578,6 +580,7 @@ export class AppMessagesManager { replyToMsgId: number, threadId: number, + groupId: string, caption: string, entities: MessageEntity[], width: number, @@ -591,7 +594,7 @@ export class AppMessagesManager { clearDraft: true, scheduleDate: number, - waveform: Uint8Array + waveform: Uint8Array, }> = {}) { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; @@ -771,6 +774,29 @@ export class AppMessagesManager { const sentDeferred = deferredPromise(); + if(preloader) { + preloader.attachPromise(sentDeferred); + sentDeferred.cancel = () => { + const error = new Error('Download canceled'); + error.name = 'AbortError'; + sentDeferred.reject(error); + }; + + sentDeferred.catch(err => { + if(err.name === 'AbortError' && !uploaded) { + this.log('cancelling upload', media); + + sentDeferred.reject(err); + this.cancelPendingMessage(message.random_id); + this.setTyping(peerId, 'sendMessageCancelAction'); + + if(uploadPromise?.cancel) { + uploadPromise.cancel(); + } + } + }); + } + const media = isDocument ? undefined : { _: photo ? 'messageMediaPhoto' : 'messageMediaDocument', pFlags: {}, @@ -821,7 +847,7 @@ export class AppMessagesManager { if(!uploaded || message.error) { uploaded = false; uploadPromise = appDownloadManager.upload(file); - preloader.attachPromise(uploadPromise); + sentDeferred.notifyAll({done: 0, total: file.size}); } let thumbUploadPromise: typeof uploadPromise; @@ -886,16 +912,7 @@ export class AppMessagesManager { const percents = Math.max(1, Math.floor(100 * progress.done / progress.total)); this.setTyping(peerId, {_: actionName, progress: percents | 0}); - }); - - uploadPromise.catch(err => { - if(err.name === 'AbortError' && !uploaded) { - this.log('cancelling upload', media); - - sentDeferred.reject(err); - this.cancelPendingMessage(message.random_id); - this.setTyping(peerId, 'sendMessageCancelAction'); - } + sentDeferred.notifyAll(progress); }); return sentDeferred; @@ -995,6 +1012,8 @@ export class AppMessagesManager { this.log('sendAlbum', files, options); + const groupId = '' + ++this.groupedTempId; + const messages = files.map((file, idx) => { const details = options.sendFileDetails[idx]; const o: any = { @@ -1004,6 +1023,7 @@ export class AppMessagesManager { silent: options.silent, replyToMsgId, threadId: options.threadId, + groupId, ...details }; @@ -1016,23 +1036,6 @@ export class AppMessagesManager { return this.sendFile(peerId, file, o).message; }); - const message = messages[messages.length - 1]; - const groupId = message.id; - messages.forEach(message => { - message.grouped_id = groupId; - }); - - const storage = options.scheduleDate ? this.getScheduledMessagesStorage(peerId) : this.getMessagesStorage(peerId); - if(options.scheduleDate) { - this.saveMessages(messages, {storage, isScheduled: true, isOutgoing: true}); - rootScope.broadcast('scheduled_new', {peerId, mid: groupId}); - } else { - this.saveMessages(messages, {storage, isOutgoing: true}); - rootScope.broadcast('history_append', {peerId, messageId: groupId, my: true}); - - this.setDialogTopMessage(message); - } - if(options.clearDraft) { appDraftsManager.syncDraft(peerId, options.threadId); } @@ -1314,10 +1317,11 @@ export class AppMessagesManager { const storage = options.isScheduled ? this.getScheduledMessagesStorage(peerId) : this.getMessagesStorage(peerId); if(options.isScheduled) { - if(!options.isGroupedItem) { - this.saveMessages([message], {storage, isScheduled: true, isOutgoing: true}); + //if(!options.isGroupedItem) { + this.saveMessages([message], {storage, isScheduled: true, isOutgoing: true}); + setTimeout(() => { rootScope.broadcast('scheduled_new', {peerId, mid: messageId}); - } + }, 0); } else { if(options.threadId && this.threadsStorage[peerId]) { delete this.threadsStorage[peerId][options.threadId]; @@ -1330,12 +1334,13 @@ export class AppMessagesManager { /* const historyStorage = this.getHistoryStorage(peerId); historyStorage.history.unshift(messageId); */ - if(!options.isGroupedItem) { - this.saveMessages([message], {storage, isOutgoing: true}); + //if(!options.isGroupedItem) { + this.saveMessages([message], {storage, isOutgoing: true}); + setTimeout(() => { rootScope.broadcast('history_append', {peerId, messageId, my: true}); this.setDialogTopMessage(message); - } + }, 0); } if(!options.isGroupedItem && options.clearDraft && !options.threadId) { @@ -1344,7 +1349,7 @@ export class AppMessagesManager { this.pendingByRandomId[message.random_id] = {peerId, tempId: messageId, storage}; - if(!options.isGroupedItem) { + if(!options.isGroupedItem && message.send) { setTimeout(message.send, 0); //setTimeout(message.send, 4000); //setTimeout(message.send, 7000); @@ -1356,7 +1361,8 @@ export class AppMessagesManager { replyToMsgId: number, threadId: number, viaBotId: number, - reply_markup: any + groupId: string, + reply_markup: any, }>) { if(options.threadId && !options.replyToMsgId) { options.replyToMsgId = options.threadId; @@ -1370,6 +1376,7 @@ export class AppMessagesManager { pFlags: this.generateFlags(peerId), date: options.scheduleDate || (tsNow(true) + serverTimeManager.serverTimeOffset), message: '', + grouped_id: options.groupId, random_id: randomLong(), reply_to: this.generateReplyHeader(options.replyToMsgId, options.threadId), via_bot_id: options.viaBotId, @@ -1446,13 +1453,26 @@ export class AppMessagesManager { } private generateForwardHeader(peerId: number, originalMessage: Message.message) { + const myId = appUsersManager.getSelf().id; + if(originalMessage.fromId === myId && !originalMessage.fwd_from) { + return; + } + const fwdHeader: MessageFwdHeader.messageFwdHeader = { _: 'messageFwdHeader', flags: 0, - date: originalMessage.date, - from_id: appPeersManager.getOutputPeer(originalMessage.fromId) + date: originalMessage.date }; + if(originalMessage.fwd_from) { + fwdHeader.from_id = originalMessage.fwd_from.from_id; + fwdHeader.from_name = originalMessage.fwd_from.from_name; + fwdHeader.post_author = originalMessage.fwd_from.post_author; + } else { + fwdHeader.from_id = appPeersManager.getOutputPeer(originalMessage.fromId); + fwdHeader.post_author = originalMessage.post_author; + } + if(appPeersManager.isBroadcast(originalMessage.peerId)) { if(originalMessage.post_author) { fwdHeader.post_author = originalMessage.post_author; @@ -1462,7 +1482,7 @@ export class AppMessagesManager { } // * there is no way to detect whether user profile is hidden - if(peerId === appUsersManager.getSelf().id) { + if(peerId === myId) { fwdHeader.saved_from_msg_id = originalMessage.id; fwdHeader.saved_from_peer = appPeersManager.getOutputPeer(originalMessage.peerId); } @@ -1733,6 +1753,13 @@ export class AppMessagesManager { peerId = appPeersManager.getPeerMigratedTo(peerId) || peerId; mids = mids.slice().sort((a, b) => a - b); + const groups: { + [groupId: string]: { + tempId: string, + messages: any[] + } + } = {}; + const newMessages = mids.map(mid => { const originalMessage = this.getMessageByPeer(fromPeerId, mid); const message = this.generateOutgoingMessage(peerId, options); @@ -1742,11 +1769,27 @@ export class AppMessagesManager { message[key] = originalMessage[key]; }); + if(originalMessage.grouped_id) { + const group = groups[originalMessage.grouped_id] ?? (groups[originalMessage.grouped_id] = {tempId: '' + ++this.groupedTempId, messages: []}); + group.messages.push(message); + } + + return message; + }); + + for(const groupId in groups) { + const group = groups[groupId]; + if(group.messages.length > 1) { + group.messages.forEach(message => { + message.grouped_id = group.tempId; + }); + } + } + + newMessages.forEach(message => { this.beforeMessageSending(message, { isScheduled: !!options.scheduleDate || undefined }); - - return message; }); const sentRequestOptions: InvokeApiOptions = {}; @@ -1754,7 +1797,7 @@ export class AppMessagesManager { sentRequestOptions.afterMessageId = this.pendingAfterMsgs[peerId].messageId; } - const promise = apiManager.invokeApiAfter('messages.forwardMessages', { + const promise = /* true ? Promise.resolve() : */apiManager.invokeApiAfter('messages.forwardMessages', { from_peer: appPeersManager.getInputPeerById(fromPeerId), id: mids.map(mid => this.getServerMessageId(mid)), random_id: newMessages.map(message => message.random_id), @@ -4860,7 +4903,7 @@ export class AppMessagesManager { this.updateMessageRepliesIfNeeded(message); - if(!message.pFlags.out && message.pFlags.unread) { + if(!message.pFlags.out && !message.pFlags.is_outgoing && message.pFlags.unread) { history.unread++; } history.count++; diff --git a/src/lib/mtproto/apiFileManager.ts b/src/lib/mtproto/apiFileManager.ts index 987f588b..c17b46a8 100644 --- a/src/lib/mtproto/apiFileManager.ts +++ b/src/lib/mtproto/apiFileManager.ts @@ -87,7 +87,7 @@ export class ApiFileManager { public downloadCheck(dcId: string | number) { const downloadPull = this.downloadPulls[dcId]; - const downloadLimit = dcId === 'upload' ? 100 : 100; + const downloadLimit = dcId === 'upload' ? 24 : 48; //const downloadLimit = Infinity; if(this.downloadActives[dcId] >= downloadLimit || !downloadPull || !downloadPull.length) { @@ -454,6 +454,10 @@ export class ApiFileManager { const id = this.tempId++; + /* setInterval(() => { + console.log(file); + }, 1e3); */ + const self = this; function* generator() { for(let offset = 0; offset < fileSize; offset += partSize) { @@ -485,6 +489,23 @@ export class ApiFileManager { } buffer = u.buffer; */ + /* setTimeout(() => { + doneParts++; + uploadResolve(); + + //////this.log('Progress', doneParts * partSize / fileSize); + + self.log('done part', part, doneParts); + + deferred.notify({done: doneParts * partSize, total: fileSize}); + + if(doneParts >= totalParts) { + deferred.resolve(resultInputFile); + resolved = true; + } + }, 1250); + return; */ + apiManager.invokeApi(method, { file_id: fileId, file_part: part, diff --git a/src/lib/mtproto/apiManager.ts b/src/lib/mtproto/apiManager.ts index 6c01a854..b0c8a17f 100644 --- a/src/lib/mtproto/apiManager.ts +++ b/src/lib/mtproto/apiManager.ts @@ -13,6 +13,7 @@ import type { MethodDeclMap } from '../../layer'; import { CancellablePromise, deferredPromise } from '../../helpers/cancellablePromise'; import { bytesFromHex, bytesToHex } from '../../helpers/bytes'; //import { clamp } from '../../helpers/number'; +import { isSafari } from '../../helpers/userAgent'; /// #if !MTPROTO_WORKER import rootScope from '../rootScope'; @@ -161,7 +162,7 @@ export class ApiManager { /// #if MTPROTO_HTTP_UPLOAD // @ts-ignore - const transportType: TransportType = connectionType === 'upload' ? 'https' : 'websocket'; + const transportType: TransportType = connectionType === 'upload' && isSafari ? 'https' : 'websocket'; //const transportType: TransportType = connectionType !== 'client' ? 'https' : 'websocket'; /// #else // @ts-ignore @@ -182,7 +183,7 @@ export class ApiManager { } const networkers = cache[dcId]; - if(networkers.length >= /* 1 */(connectionType === 'client' ? 1 : (connectionType === 'download' ? 3 : 3))) { + if(networkers.length >= /* 1 */(connectionType === 'client' || transportType === 'https' ? 1 : (connectionType === 'download' ? 3 : 3))) { let i = networkers.length - 1, found = false; for(; i >= 0; --i) { if(networkers[i].isOnline) { diff --git a/src/lib/mtproto/dcConfigurator.ts b/src/lib/mtproto/dcConfigurator.ts index d61229e2..6fa12d91 100644 --- a/src/lib/mtproto/dcConfigurator.ts +++ b/src/lib/mtproto/dcConfigurator.ts @@ -43,6 +43,7 @@ export class DcConfigurator { private chosenServers: Servers = {} as any; + /// #if !MTPROTO_HTTP private transportSocket = (dcId: number, connectionType: ConnectionType) => { const subdomain = this.sslSubdomains[dcId - 1]; const path = Modes.test ? 'apiws_test' : 'apiws'; @@ -50,7 +51,9 @@ export class DcConfigurator { const suffix = connectionType === 'upload' ? '-U' : connectionType === 'download' ? '-D' : ''; return new Socket(dcId, chosenServer, suffix, connectionType === 'client' ? 30000 : 10000); }; + /// #endif + /// #if MTPROTO_HTTP_UPLOAD || MTPROTO_HTTP private transportHTTP = (dcId: number, connectionType: ConnectionType) => { if(Modes.ssl || !Modes.http) { const subdomain = this.sslSubdomains[dcId - 1] + (connectionType !== 'client' ? '-1' : ''); @@ -66,6 +69,7 @@ export class DcConfigurator { } } }; + /// #endif public chooseServer(dcId: number, connectionType: ConnectionType = 'client', transportType: TransportType = 'websocket', reuse = true) { /* if(transportType === 'websocket' && !Modes.multipleConnections) { diff --git a/src/lib/mtproto/mtproto.worker.ts b/src/lib/mtproto/mtproto.worker.ts index fa624eda..0fce0cf8 100644 --- a/src/lib/mtproto/mtproto.worker.ts +++ b/src/lib/mtproto/mtproto.worker.ts @@ -7,11 +7,10 @@ import networkerFactory from "./networkerFactory"; import apiFileManager from './apiFileManager'; //import { logger, LogLevels } from '../logger'; import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from './mtproto.service'; +import { ctx } from '../../helpers/userAgent'; //const log = logger('DW', LogLevels.error); -const ctx = self as any as DedicatedWorkerGlobalScope; - //console.error('INCLUDE !!!', new Error().stack); /* function isObject(object: any) { @@ -64,7 +63,15 @@ networkerFactory.onConnectionStatusChange = (status) => { respond({type: 'connectionStatusChange', payload: status}); }; -ctx.addEventListener('message', async(e) => { +/* ctx.onerror = (error) => { + console.error('error:', error); +}; + +ctx.onunhandledrejection = (error) => { + console.error('onunhandledrejection:', error); +}; */ + +const onMessage = async(e: any) => { try { const task = e.data; const taskId = task.taskId; @@ -103,6 +110,10 @@ ctx.addEventListener('message', async(e) => { webpSupported = task.payload; return; } + + if(!task.task) { + return; + } switch(task.task) { case 'computeSRP': @@ -131,6 +142,8 @@ ctx.addEventListener('message', async(e) => { } catch(error) { respond({taskId, error}); } + + break; } case 'getNetworker': { @@ -157,12 +170,15 @@ ctx.addEventListener('message', async(e) => { } //throw new Error('Unknown task: ' + task.task); + break; } } } catch(err) { } -}); +}; + +ctx.addEventListener('message', onMessage); //console.log('[WORKER] Will send ready', Date.now() / 1000); ctx.postMessage('ready'); diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index a30dec54..a23bb281 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -1,4 +1,5 @@ import MTProtoWorker from 'worker-loader!./mtproto.worker'; +//import './mtproto.worker'; import { isObject } from '../../helpers/object'; import type { MethodDeclMap } from '../../layer'; import type { InvokeApiOptions } from '../../types'; @@ -9,7 +10,7 @@ import webpWorkerController from '../webp/webpWorkerController'; import type { DownloadOptions } from './apiFileManager'; import { ApiError } from './apiManager'; import type { ServiceWorkerTask, ServiceWorkerTaskResponse } from './mtproto.service'; -import { MOUNT_CLASS_TO, UserAuth } from './mtproto_config'; +import { DEBUG, MOUNT_CLASS_TO, UserAuth } from './mtproto_config'; import type { MTMessage } from './networker'; import referenceDatabase from './referenceDatabase'; import appDocsManager from '../appManagers/appDocsManager'; @@ -32,7 +33,7 @@ type HashOptions = { }; export class ApiManagerProxy extends CryptoWorkerMethods { - public worker: Worker; + public worker: /* Window */Worker; public postMessage: (...args: any[]) => void; private afterMessageIdTemp = 0; @@ -54,6 +55,8 @@ export class ApiManagerProxy extends CryptoWorkerMethods { private isSWRegistered = true; + private debug = DEBUG; + constructor() { super(); this.log('constructor'); @@ -118,6 +121,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { //return; const worker = new MTProtoWorker(); + //const worker = window; worker.addEventListener('message', (e) => { if(!this.worker) { this.worker = worker; @@ -182,23 +186,27 @@ export class ApiManagerProxy extends CryptoWorkerMethods { } else { navigator.serviceWorker.controller.postMessage(task); } - } else { + } else if(task.hasOwnProperty('result') || task.hasOwnProperty('error')) { this.finalizeTask(task.taskId, task.result, task.error); } }); + + worker.addEventListener('error', (err) => { + this.log.error('WORKER ERROR', err); + }); } private finalizeTask(taskId: number, result: any, error: any) { const deferred = this.awaiting[taskId]; if(deferred !== undefined) { - this.log.debug('done', deferred.taskName, result, error); + this.debug && this.log.debug('done', deferred.taskName, result, error); error ? deferred.reject(error) : deferred.resolve(result); delete this.awaiting[taskId]; } } public performTaskWorker(task: string, ...args: any[]) { - this.log.debug('start', task, args); + this.debug && this.log.debug('start', task, args); return new Promise((resolve, reject) => { this.awaiting[this.taskId] = {resolve, reject, taskName: task}; @@ -218,12 +226,12 @@ export class ApiManagerProxy extends CryptoWorkerMethods { private releasePending() { if(this.postMessage) { - this.log.debug('releasing tasks, length:', this.pending.length); + this.debug && this.log.debug('releasing tasks, length:', this.pending.length); this.pending.forEach(pending => { this.postMessage(pending); }); - this.log.debug('released tasks'); + this.debug && this.log.debug('released tasks'); this.pending.length = 0; } } @@ -262,7 +270,7 @@ export class ApiManagerProxy extends CryptoWorkerMethods { return this.performTaskWorker('invokeApi', method, params, options).then((result: any) => { if(result._.includes('NotModified')) { - this.log.warn('NotModified saved!', method, queryJSON); + this.debug && this.log.warn('NotModified saved!', method, queryJSON); return cached.result; } diff --git a/src/lib/mtproto/networker.ts b/src/lib/mtproto/networker.ts index 1c639199..35710a77 100644 --- a/src/lib/mtproto/networker.ts +++ b/src/lib/mtproto/networker.ts @@ -13,6 +13,8 @@ import { longToBytes } from '../crypto/crypto_utils'; import MTTransport from './transports/transport'; import { convertToUint8Array, bufferConcat, bytesCmp, bytesToHex } from '../../helpers/bytes'; import { nextRandomInt, randomLong } from '../../helpers/random'; +import { CancellablePromise, deferredPromise } from '../../helpers/cancellablePromise'; +import { isSafari } from '../../helpers/userAgent'; /// #if MTPROTO_HTTP_UPLOAD // @ts-ignore @@ -112,6 +114,9 @@ export default class MTPNetworker { public isOnline = false; private lastResponseTime = 0; private disconnectDelay: number; + private pingPromise: CancellablePromise; + private sentPingTimes = 0; + private tt = 0; //public onConnectionStatusChange: (online: boolean) => void; constructor(public dcId: number, private authKey: number[], private authKeyId: Uint8Array, @@ -161,9 +166,13 @@ export default class MTPNetworker { // * handle outcoming dead socket, server will close the connection if((this.transport as Socket).networker) { - this.disconnectDelay = (this.transport as Socket).retryTimeout / 1000 | 0; - setInterval(this.sendPingDelayDisconnect, (this.disconnectDelay - 5) * 1000); - this.sendPingDelayDisconnect(); + if(isSafari) { + this.pingPromise = Promise.resolve(); + } else { + this.disconnectDelay = (this.transport as Socket).retryTimeout / 1000 | 0; + //setInterval(this.sendPingDelayDisconnect, (this.disconnectDelay - 5) * 1000); + this.sendPingDelayDisconnect(); + } } } @@ -326,13 +335,63 @@ export default class MTPNetworker { } private sendPingDelayDisconnect = () => { - if(!this.isOnline) return; // * already disconnected - this.wrapMtpCall('ping_delay_disconnect', { + if(this.pingPromise) return; + + if(!this.isOnline) { + if((this.transport as Socket).connected) { + (this.transport as Socket).handleClose(); + } + + return; + } + + this.log('sendPingDelayDisconnect', this.sentPingTimes); + + /* if(this.tt) clearTimeout(this.tt); + this.tt = self.setTimeout(() => { + (this.transport as any).ws.close(1000); + this.tt = 0; + }, this.disconnectDelay * 1000); */ + /* this.wrapMtpCall('ping_delay_disconnect', { ping_id: randomLong(), disconnect_delay: this.disconnectDelay }, { noResponse: true, notContentRelated: true + }); */ + const deferred = this.pingPromise = deferredPromise(); + + const timeoutTime = this.disconnectDelay * 1000; + + /* if(!this.sentPingTimes || true) { + ++this.sentPingTimes; */ + const startTime = Date.now(); + this.wrapMtpCall('ping', { + ping_id: randomLong() + }, {}).then(pong => { + const elapsedTime = Date.now() - startTime; + this.log('sendPingDelayDisconnect: response', pong, elapsedTime > timeoutTime); + + if(elapsedTime > timeoutTime) { + deferred.reject(); + } else { + setTimeout(deferred.resolve, timeoutTime - elapsedTime); + } + }, deferred.reject).finally(() => { + clearTimeout(rejectTimeout); + //--this.sentPingTimes; + }); + //} + + const rejectTimeout = self.setTimeout(deferred.reject, timeoutTime); + + deferred.catch(() => { + (this.transport as Socket).handleClose(); + }); + + deferred.finally(() => { + this.pingPromise = null; + this.sendPingDelayDisconnect(); }); }; @@ -543,6 +602,10 @@ export default class MTPNetworker { this.log.error('timeout', message); this.setConnectionStatus(false); + + this.getEncryptedOutput(message).then(bytes => { + this.log.error('timeout encrypted', bytes); + }); }, CONNECTION_TIMEOUT); promise.finally(() => { @@ -571,7 +634,11 @@ export default class MTPNetworker { }); } - this.sendPingDelayDisconnect(); + if(!this.pingPromise && (this.transport as Socket).networker) { + this.sendPingDelayDisconnect(); + } + /* this.sentPingTimes = 0; + this.sendPingDelayDisconnect(); */ } /* if(this.onConnectionStatusChange) { this.onConnectionStatusChange(this.isOnline); @@ -845,10 +912,14 @@ export default class MTPNetworker { }); } - public sendEncryptedRequest(message: MTMessage) { + public getEncryptedOutput(message: MTMessage) { /* if(DEBUG) { this.log.debug('Send encrypted', message, this.authKeyId); } */ + /* if(!this.isOnline) { + this.log('trying to send message when offline:', Object.assign({}, message)); + //debugger; + } */ const data = new TLSerialization({ startMaxLength: message.body.length + 2048 @@ -859,26 +930,48 @@ export default class MTPNetworker { data.storeLong(message.msg_id, 'message_id'); data.storeInt(message.seq_no, 'seq_no'); - + data.storeInt(message.body.length, 'message_data_length'); data.storeRawBytes(message.body, 'message_data'); + + /* const messageDataLength = message.body.length; + let canBeLength = 0; // bytes + canBeLength += 8; + canBeLength += 8; + canBeLength += 8; + canBeLength += 4; + canBeLength += 4; + canBeLength += message.body.length; */ const dataBuffer = data.getBuffer(); - + + /* if(dataBuffer.byteLength !== canBeLength || !bytesCmp(new Uint8Array(dataBuffer.slice(dataBuffer.byteLength - message.body.length)), new Uint8Array(message.body))) { + this.log.error('wrong length', dataBuffer, canBeLength, message.msg_id); + } */ + const paddingLength = (16 - (data.offset % 16)) + 16 * (1 + nextRandomInt(5)); const padding = [...new Uint8Array(paddingLength).randomize()]; + /* const padding = [167, 148, 207, 226, 86, 192, 193, 57, 124, 153, 174, 145, 159, 1, 5, 70, 127, 157, + 51, 241, 46, 85, 141, 212, 139, 234, 213, 164, 197, 116, 245, 70, 184, 40, 40, 201, 233, 211, 150, + 94, 57, 84, 1, 135, 108, 253, 34, 139, 222, 208, 71, 214, 90, 67, 36, 28, 167, 148, 207, 226, 86, 192, 193, 57, 124, 153, 174, 145, 159, 1, 5, 70, 127, 157, + 51, 241, 46, 85, 141, 212, 139, 234, 213, 164, 197, 116, 245, 70, 184, 40, 40, 201, 233, 211, 150, + 94, 57, 84, 1, 135, 108, 253, 34, 139, 222, 208, 71, 214, 90, 67, 36, 28].slice(0, paddingLength); */ const dataWithPadding = bufferConcat(dataBuffer, padding); // this.log('Adding padding', dataBuffer, padding, dataWithPadding) // this.log('auth_key_id', bytesToHex(self.authKeyID)) - /* if(message.fileUpload) { + /* if(dataWithPadding.byteLength % 16) { + this.log.error('aaa', dataWithPadding, paddingLength); + } + + if(message.fileUpload) { this.log('Send encrypted: body length:', (message.body as ArrayBuffer).byteLength, paddingLength, dataWithPadding); } */ - + return this.getEncryptedMessage(dataWithPadding).then((encryptedResult) => { /* if(DEBUG) { - this.log.debug('Got encrypted out message', encryptedResult); + this.log('Got encrypted out message', encryptedResult); } */ const request = new TLSerialization({ @@ -890,10 +983,17 @@ export default class MTPNetworker { const requestData = request.getBytes(true); - /* if(message.fileUpload) { - this.log('Send encrypted: requestData length:', requestData.length, requestData.length % 16, paddingLength % 16, paddingLength, data.offset); - } */ + //if(message.fileUpload) { + //this.log('Send encrypted: requestData length:', requestData.length, requestData.length % 16, paddingLength % 16, paddingLength, data.offset, encryptedResult.msgKey.length % 16, encryptedResult.bytes.length % 16); + //this.log('Send encrypted: messageId:', message.msg_id, requestData.length); + //} + + return requestData; + }); + } + public sendEncryptedRequest(message: MTMessage) { + return this.getEncryptedOutput(message).then(requestData => { const promise = this.transport.send(requestData); /// #if !MTPROTO_HTTP && !MTPROTO_HTTP_UPLOAD return promise; @@ -1381,17 +1481,16 @@ export default class MTPNetworker { break; } - case 'pong': { // * https://core.telegram.org/mtproto/service_messages#ping-messages-pingpong - These messages do not require acknowledgments - /* if((this.transport as Socket).networker) { + case 'pong': { // * https://core.telegram.org/mtproto/service_messages#ping-messages-pingpong - These messages doesn't require acknowledgments + if((this.transport as Socket).networker) { const sentMessageId = message.msg_id; - const sentMessage = this.sentMessages[sentMessageId]; + const sentMessage = this.sentMessages[sentMessageId]; if(sentMessage) { + sentMessage.deferred.resolve(message); delete this.sentMessages[sentMessageId]; } - - - } */ + } break; } diff --git a/src/lib/mtproto/timeManager.ts b/src/lib/mtproto/timeManager.ts index 7f0bfc85..4ed40b19 100644 --- a/src/lib/mtproto/timeManager.ts +++ b/src/lib/mtproto/timeManager.ts @@ -1,6 +1,14 @@ import sessionStorage from '../sessionStorage'; import { longFromInts } from './bin_utils'; import { nextRandomInt } from '../../helpers/random'; +import { MOUNT_CLASS_TO } from './mtproto_config'; + +/* +let lol: any = {}; +for(var i = 0; i < 100; i++) { + timeManager.generateId(); +} +*/ export class TimeManager { private lastMessageId = [0, 0]; @@ -30,7 +38,12 @@ export class TimeManager { const ret = longFromInts(messageId[0], messageId[1]); - //console.log('[TimeManager]: Generated msg id', messageId, this.timeOffset, ret); + /* if(lol[ret]) { + console.error('[TimeManager]: Generated SAME msg id', messageId, this.timeOffset, ret); + } + lol[ret] = true; + + console.log('[TimeManager]: Generated msg id', messageId, this.timeOffset, ret); */ return ret } @@ -52,4 +65,6 @@ export class TimeManager { } } -export default new TimeManager(); +const timeManager = new TimeManager(); +MOUNT_CLASS_TO && (MOUNT_CLASS_TO.timeManager = timeManager); +export default timeManager; diff --git a/src/lib/mtproto/tl_utils.ts b/src/lib/mtproto/tl_utils.ts index e85f5194..87a2f558 100644 --- a/src/lib/mtproto/tl_utils.ts +++ b/src/lib/mtproto/tl_utils.ts @@ -25,7 +25,7 @@ class TLSerialization { public byteView: Uint8Array; constructor(options: Partial<{startMaxLength: number, mtproto: true}> = {}) { - this.maxLength = options.startMaxLength || 2048 // 2Kb + this.maxLength = options.startMaxLength || 2048; // 2Kb this.mtproto = options.mtproto || false; this.createBuffer(); } @@ -38,11 +38,22 @@ class TLSerialization { public getArray() { const resultBuffer = new ArrayBuffer(this.offset); - const resultArray = new Int32Array(resultBuffer); - - resultArray.set(this.intView.subarray(0, this.offset / 4)); + + //let perf = performance.now(); + /* const resultUint8: any = new Uint8Array(resultBuffer); + resultUint8.set(this.byteView.subarray(0, this.offset)); */ + //console.log('perf uint8', performance.now() - perf); + + //perf = performance.now(); + const resultInt32 = new Int32Array(resultBuffer); + resultInt32.set(this.intView.subarray(0, this.offset / 4)); + //console.log('perf int32', performance.now() - perf); + + /* if(resultUint8.buffer.byteLength !== resultInt32.buffer.byteLength) { + console.error(resultUint8, resultInt32); + } */ - return resultArray; + return resultInt32; } public getBuffer() { diff --git a/src/lib/mtproto/transports/obfuscation.ts b/src/lib/mtproto/transports/obfuscation.ts index 8a1d5faf..ed623d70 100644 --- a/src/lib/mtproto/transports/obfuscation.ts +++ b/src/lib/mtproto/transports/obfuscation.ts @@ -1,8 +1,55 @@ //import aesjs from 'aes-js'; -import { CTR } from "@cryptography/aes"; +import AES from "@cryptography/aes"; import { bytesFromWordss } from "../../../helpers/bytes"; import { Codec } from "./codec"; +class Counter { + _counter: Uint8Array; + + constructor(initialValue: Uint8Array) { + this._counter = initialValue; + } + + increment() { + for(let i = 15; i >= 0; i--) { + if(this._counter[i] === 255) { + this._counter[i] = 0; + } else { + this._counter[i]++; + break; + } + } + } +} + +class CTR { + _counter: Counter; + _remainingCounter: Uint8Array = null; + _remainingCounterIndex = 16; + _aes: AES; + + constructor(key: Uint8Array, counter: Uint8Array) { + this._counter = new Counter(counter); + this._aes = new AES(key); + } + + update(payload: Uint8Array) { + const encrypted = payload.slice(); + + for(let i = 0; i < encrypted.length; i++) { + if(this._remainingCounterIndex === 16) { + this._remainingCounter = new Uint8Array(bytesFromWordss(this._aes.encrypt(this._counter._counter))); + this._remainingCounterIndex = 0; + this._counter.increment(); + } + + encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++]; + } + + return encrypted; + } +} + /* @cryptography/aes не работает с массивами которые не кратны 4, поэтому использую intermediate а не abridged */ @@ -98,6 +145,13 @@ export default class Obfuscation { return res; } */ public encode(payload: Uint8Array) { + return this.encNew.update(payload); + } + + public decode(payload: Uint8Array) { + return this.decNew.update(payload); + } + /* public encode(payload: Uint8Array) { let res = this.encNew.encrypt(payload); let bytes = new Uint8Array(bytesFromWordss(res)); @@ -109,5 +163,5 @@ export default class Obfuscation { let bytes = new Uint8Array(bytesFromWordss(res)); return bytes; - } + } */ } \ No newline at end of file diff --git a/src/lib/mtproto/transports/websocket.ts b/src/lib/mtproto/transports/websocket.ts index 3b17526b..97863429 100644 --- a/src/lib/mtproto/transports/websocket.ts +++ b/src/lib/mtproto/transports/websocket.ts @@ -1,39 +1,38 @@ import MTTransport from './transport'; -//import abridgetPacketCodec from './abridged'; +//import abridgedPacketCodec from './abridged'; import intermediatePacketCodec from './intermediate'; import MTPNetworker from '../networker'; import { logger, LogLevels } from '../../logger'; import Obfuscation from './obfuscation'; import { DEBUG, Modes } from '../mtproto_config'; -//import { debounce } from '../../../helpers/schedulers'; export default class Socket extends MTTransport { - ws: WebSocket; + public ws: WebSocket; - pending: Array> = []; - connected = false; - - transport = 'websocket'; - - obfuscation = new Obfuscation(); - - networker: MTPNetworker; + public connected = false; + private codec = intermediatePacketCodec; + private log: ReturnType; + private obfuscation = new Obfuscation(); + public networker: MTPNetworker; - log: ReturnType; + private lastCloseTime: number; - codec = intermediatePacketCodec; + private debug = Modes.debug && false; + //private releasePendingDebounced: () => void; - lastCloseTime: number; - - debug = Modes.debug; - //releasePendingDebounced: () => void; + /* private stream: Array; + private canRead: Promise; + private resolveRead: () => void; */ + //private lol: Uint8Array[] = []; + //private dd: () => void; constructor(dcId: number, url: string, logSuffix: string, public retryTimeout: number) { super(dcId, url); @@ -43,18 +42,37 @@ export default class Socket extends MTTransport { this.log = logger(`WS-${dcId}` + logSuffix, logLevel); this.log('constructor'); this.connect(); - //this.releasePendingDebounced = debounce(() => this.releasePending(true), 2000, false, true); + //this.releasePendingDebounced = debounce(() => this.releasePending(true), 200, false, true); + + /* this.dd = debounce(() => { + if(this.connected && this.lol.length) { + this.ws.send(this.lol.shift()); + + if(this.lol.length) { + this.dd(); + } + } + }, 100, false, true); */ + } + + private removeListeners() { + this.ws.removeEventListener('open', this.handleOpen); + this.ws.removeEventListener('close', this.handleClose); + this.ws.removeEventListener('error', this.handleError); + this.ws.removeEventListener('message', this.handleMessage); } connect = () => { if(this.ws) { - this.ws.removeEventListener('open', this.handleOpen); - this.ws.removeEventListener('close', this.handleClose); - this.ws.removeEventListener('error', this.handleError); - this.ws.removeEventListener('message', this.handleMessage); + this.removeListeners(); this.ws.close(1000); } + /* this.stream = []; + this.canRead = new Promise(resolve => { + this.resolveRead = resolve; + }); */ + this.ws = new WebSocket(this.url, 'binary'); this.ws.binaryType = 'arraybuffer'; this.ws.addEventListener('open', this.handleOpen); @@ -89,9 +107,10 @@ export default class Socket extends MTTransport { this.log.error(e); }; - handleClose = (event: CloseEvent) => { - this.log('closed', event, this.pending, this.ws.bufferedAmount); + handleClose = () => { + this.log('closed'/* , event, this.pending, this.ws.bufferedAmount */); this.connected = false; + this.removeListeners(); const time = Date.now(); const diff = time - this.lastCloseTime; @@ -117,7 +136,7 @@ export default class Socket extends MTTransport { }; handleMessage = (event: MessageEvent) => { - this.debug && this.log.debug('<-', 'handleMessage', event); + this.debug && this.log.debug('<-', 'handleMessage', /* event, */event.data.byteLength); let data = this.obfuscation.decode(new Uint8Array(event.data)); data = this.codec.readPacket(data); @@ -125,11 +144,23 @@ export default class Socket extends MTTransport { if(this.networker) { // authenticated! //this.pending = this.pending.filter(p => p.body); // clear pending - this.debug && this.log.debug('redirecting to networker'); - return this.networker.parseResponse(data).then(response => { + this.debug && this.log.debug('redirecting to networker', data.length); + this.networker.parseResponse(data).then(response => { this.debug && this.log.debug('redirecting to networker response:', response); - this.networker.processMessage(response.response, response.messageId, response.sessionId); + + try { + this.networker.processMessage(response.response, response.messageId, response.sessionId); + } catch(err) { + this.log.error('handleMessage networker processMessage error', err); + } + + //this.releasePending(); + }).catch(err => { + this.log.error('handleMessage networker parseResponse error', err); }); + + //this.dd(); + return; } //console.log('got hex:', data.hex); @@ -159,7 +190,7 @@ export default class Socket extends MTTransport { return promise; } - } + }; releasePending(/* tt = false */) { if(!this.connected) { @@ -172,10 +203,14 @@ export default class Socket extends MTTransport { return; } */ - //this.log.error('Pending length:', this.pending.length); + //this.log('-> messages to send:', this.pending.length); let length = this.pending.length; //for(let i = length - 1; i >= 0; --i) { for(let i = 0; i < length; ++i) { + /* if(this.ws.bufferedAmount) { + break; + } */ + const pending = this.pending[i]; const {body, bodySent} = pending; if(body && !bodySent) { @@ -190,13 +225,17 @@ export default class Socket extends MTTransport { this.log.error('bufferedAmount:', this.ws.bufferedAmount); } */ - if(this.ws.readyState !== this.ws.OPEN) { + /* if(this.ws.readyState !== this.ws.OPEN) { this.log.error('ws is closed?'); this.connected = false; break; - } + } */ - this.ws.send(enc); + //this.lol.push(enc); + //setTimeout(() => { + this.ws.send(enc); + //}, 100); + //this.dd(); if(!pending.resolve) { // remove if no response needed this.pending.splice(i--, 1); diff --git a/webpack.common.js b/webpack.common.js index aeb94aef..d7dac3d3 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -18,7 +18,7 @@ if(devMode) { const opts = { MTPROTO_WORKER: true, MTPROTO_HTTP: false, - MTPROTO_HTTP_UPLOAD: false, + MTPROTO_HTTP_UPLOAD: true, DEBUG: devMode, version: 3, "ifdef-verbose": devMode, // add this for verbose output @@ -87,8 +87,8 @@ module.exports = { output: { path: path.resolve(__dirname, 'public'), - filename: "[name].bundle.js", - chunkFilename: "[name].chunk.js" + filename: "[name].[chunkhash].bundle.js", + chunkFilename: "[name].[chunkhash].chunk.js" }, devServer: { @@ -164,8 +164,8 @@ module.exports = { new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional - filename: '[name].css', - chunkFilename: '[id].css', + filename: '[name].[contenthash].css', + chunkFilename: '[id].[contenthash].css', }), new MediaQueryPlugin({