diff --git a/src/components/appMediaViewerBase.ts b/src/components/appMediaViewerBase.ts index f285e848..9a35ddd0 100644 --- a/src/components/appMediaViewerBase.ts +++ b/src/components/appMediaViewerBase.ts @@ -245,6 +245,11 @@ export default class AppMediaViewerBase< // * constructing html end + this.listLoader.onLoadedMore = () => { + this.buttons.prev.classList.toggle('hide', !this.listLoader.previous.length); + this.buttons.next.classList.toggle('hide', !this.listLoader.next.length); + }; + this.setNewMover(); } @@ -1173,9 +1178,11 @@ export default class AppMediaViewerBase< } */ } - /* if(this.nextTargets.length < 10 && this.loadMore) { - this.loadMore(); - } */ + if(this.listLoader.next.length < 10) { + setTimeout(() => { + this.listLoader.load(true); + }, 0); + } //if(prevTarget && (!prevTarget.parentElement || !this.isElementVisible(this.targetContainer, prevTarget))) prevTarget = null; //if(nextTarget && (!nextTarget.parentElement || !this.isElementVisible(this.targetContainer, nextTarget))) nextTarget = null; diff --git a/src/components/avatar.ts b/src/components/avatar.ts index 1ae73511..2bf55a31 100644 --- a/src/components/avatar.ts +++ b/src/components/avatar.ts @@ -101,7 +101,7 @@ export async function openAvatarViewer( } if(photo) { - if(!isObject(message)) { + if(!isObject(message) && message) { photo = appPhotosManager.getPhoto(message); } diff --git a/src/components/passwordInputField.ts b/src/components/passwordInputField.ts index 6c948b47..ff6cb370 100644 --- a/src/components/passwordInputField.ts +++ b/src/components/passwordInputField.ts @@ -4,6 +4,7 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +// import { IS_MOBILE_SAFARI, IS_SAFARI } from "../environment/userAgent"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import InputField, { InputFieldOptions } from "./inputField"; @@ -21,12 +22,15 @@ export default class PasswordInputField extends InputField { const input = this.input as HTMLInputElement; input.type = 'password'; input.setAttribute('required', ''); + input.name = 'notsearch_password'; input.autocomplete = 'off'; - /* input.readOnly = true; - input.addEventListener('focus', () => { - input.removeAttribute('readonly'); - }, {once: true}); */ + /* if(IS_SAFARI && !IS_MOBILE_SAFARI) { + input.setAttribute('readonly', ''); + input.addEventListener('focus', () => { + input.removeAttribute('readonly'); + }, {once: true}); + } */ // * https://stackoverflow.com/a/35949954/6758968 const stealthy = document.createElement('input'); diff --git a/src/config/modes.ts b/src/config/modes.ts index a8cbc7b6..9c871268 100644 --- a/src/config/modes.ts +++ b/src/config/modes.ts @@ -9,15 +9,30 @@ * https://github.com/zhukov/webogram/blob/master/LICENSE */ +import type { TransportType } from "../lib/mtproto/dcConfigurator"; + const Modes = { test: location.search.indexOf('test=1') > 0/* || true */, debug: location.search.indexOf('debug=1') > 0, - http: false, //location.search.indexOf('http=1') > 0, + http: false, ssl: true, // location.search.indexOf('ssl=1') > 0 || location.protocol === 'https:' && location.search.indexOf('ssl=0') === -1, multipleConnections: true, - asServiceWorker: false + asServiceWorker: false, + transport: 'websocket' as TransportType }; +/// #if MTPROTO_HAS_HTTP +Modes.http = location.search.indexOf('http=1') > 0; +/// #endif + +/// #if MTPROTO_HTTP || !MTPROTO_HAS_WS +Modes.http = true; +/// #endif + +if(Modes.http) { + Modes.transport = 'https'; +} + /// #if MTPROTO_SW Modes.asServiceWorker = true; /// #endif diff --git a/src/helpers/avatarListLoader.ts b/src/helpers/avatarListLoader.ts index af7da474..14b0e010 100644 --- a/src/helpers/avatarListLoader.ts +++ b/src/helpers/avatarListLoader.ts @@ -17,7 +17,7 @@ export default class AvatarListLoader loadMore: (anchor, older, loadCount) => { if(this.peerId.isAnyChat() || !older) return Promise.resolve({count: 0, items: []}); // ! это значит, что открыло аватар чата, но следующих фотографий нет. - const maxId = anchor?.photoId; + const maxId = anchor?.photoId || this.current?.photoId; return appPhotosManager.getUserPhotos(this.peerId, maxId, loadCount).then(value => { const items = value.photos.map(photoId => { return {element: null as HTMLElement, photoId} as any; diff --git a/src/helpers/date.ts b/src/helpers/date.ts index 435baf70..c459d017 100644 --- a/src/helpers/date.ts +++ b/src/helpers/date.ts @@ -47,7 +47,9 @@ export function formatDateAccordingToTodayNew(time: Date) { }).element; } -export function formatFullSentTime(timestamp: number) { +export function formatFullSentTimeRaw(timestamp: number, options: { + capitalize?: boolean +} = {}) { const date = new Date(); const time = new Date(timestamp * 1000); const now = date.getTime() / 1000; @@ -56,9 +58,13 @@ export function formatFullSentTime(timestamp: number) { let dateEl: Node | string; if((now - timestamp) < ONE_DAY && date.getDate() === time.getDate()) { // if the same day - dateEl = i18n('Date.Today'); + dateEl = i18n(options.capitalize ? 'Date.Today' : 'Peer.Status.Today'); } else if((now - timestamp) < (ONE_DAY * 2) && (date.getDate() - 1) === time.getDate()) { // yesterday - dateEl = capitalizeFirstLetter(I18n.format('Yesterday', true)); + dateEl = i18n(options.capitalize ? 'Yesterday' : 'Peer.Status.Yesterday'); + + if(options.capitalize) { + (dateEl as HTMLElement).style.textTransform = 'capitalize'; + } } else if(date.getFullYear() !== time.getFullYear()) { // different year dateEl = new I18n.IntlDateElement({ date: time, @@ -80,6 +86,14 @@ export function formatFullSentTime(timestamp: number) { // dateStr = months[time.getMonth()].slice(0, 3) + ' ' + time.getDate(); } + return {dateEl, timeEl}; +} + +export function formatFullSentTime(timestamp: number) { + const {dateEl, timeEl} = formatFullSentTimeRaw(timestamp, { + capitalize: true + }); + const fragment = document.createDocumentFragment(); fragment.append(dateEl, ' ', i18n('ScheduleController.at'), ' ', timeEl); return fragment; diff --git a/src/lang.ts b/src/lang.ts index d95dbfa7..fd1df318 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -480,9 +480,6 @@ const lang = { "Emoji": "Emoji", "AddContactTitle": "Add Contact", "HiddenName": "Deleted Account", - "ActionGroupCallInvited": "un1 invited un2 to the voice chat", - "ActionGroupCallYouInvited": "You invited un2 to the voice chat", - "ActionGroupCallInvitedYou": "un1 invited you to the voice chat", "Seconds": { "one_value": "%1$d second", "other_value": "%1$d seconds" @@ -655,6 +652,9 @@ const lang = { "Chat.Service.VoiceChatFinished": "%1$@ ended the video chat (%2$@)", "Chat.Service.VoiceChatFinishedYou": "You ended the video chat (%@)", "Chat.Service.VoiceChatFinished.Channel": "Live Stream ended (%1$@)", + "Chat.Service.VoiceChatInvitation": "%1$@ invited %2$@ to the [video chat](open)", + "Chat.Service.VoiceChatInvitationByYou": "You invited %1$@ to the [video chat](open)", + "Chat.Service.VoiceChatInvitationForYou": "%1$@ invited you to the [video chat](open)", "ChatList.Service.VoiceChatScheduled": "%1$@ scheduled a video chat for %2$@", "ChatList.Service.VoiceChatScheduledYou": "You scheduled a video chat for %2$@", "Chat.Poll.Unvote": "Retract Vote", diff --git a/src/langSign.ts b/src/langSign.ts index 06d91fba..098b8a4f 100644 --- a/src/langSign.ts +++ b/src/langSign.ts @@ -28,7 +28,7 @@ const lang = { "Login.ContinueOnLanguage": "Continue in English", "Login.QR.Title": "Log in to Telegram by QR Code", "Login.QR.Help1": "Open Telegram on your phone", - "Login.QR.Help2": "Go to **Settings** > **Devices** > **Scan QR**", + "Login.QR.Help2": "Go to **Settings** > **Devices** > **Link Desktop Device**", "Login.QR.Help3": "Point your phone at this screen to confirm login", "Login.QR.Cancel": "Log in by phone Number", "Login.QR.Login": "Log in by QR Code", diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index fcdbe823..9926dd7b 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -2933,6 +2933,20 @@ export class AppMessagesManager { return el; } + private wrapJoinVoiceChatAnchor(message: Message.messageService) { + const action = message.action as MessageAction.messageActionInviteToGroupCall; + const {onclick, url} = RichTextProcessor.wrapUrl(`tg://voicechat?chat_id=${message.peerId.toChatId()}&id=${action.call.id}&access_hash=${action.call.access_hash}`); + if(!onclick) { + return document.createElement('span'); + } + + const a = document.createElement('a'); + a.href = url; + a.setAttribute('onclick', onclick + '(this)'); + + return a; + } + private wrapMessageActionTextNewUnsafe(message: MyMessage, plain?: boolean) { const element: HTMLElement = plain ? undefined : document.createElement('span'); const action = 'action' in message && message.action; @@ -2976,16 +2990,7 @@ export class AppMessagesManager { if(action.duration !== undefined) { args.push(formatCallDuration(action.duration, plain)); } else { - const {onclick, url} = RichTextProcessor.wrapUrl(`tg://voicechat?chat_id=${message.peerId.toChatId()}&id=${action.call.id}&access_hash=${action.call.access_hash}`); - if(!onclick) { - args.push(document.createElement('span')); - break; - } - - const a = document.createElement('a'); - a.href = url; - a.setAttribute('onclick', onclick + '(this)'); - args.push(a); + args.push(this.wrapJoinVoiceChatAnchor(message as any)); } break; @@ -2993,15 +2998,15 @@ export class AppMessagesManager { case 'messageActionInviteToGroupCall': { const peerIds = [message.fromId, action.users[0].toPeerId()]; - let a = 'ActionGroupCall'; + let a = 'Chat.Service.VoiceChatInvitation'; const myId = appUsersManager.getSelf().id; - if(peerIds[0] === myId) a += 'You'; - a += 'Invited'; - if(peerIds[1] === myId) a += 'You'; + if(peerIds[0] === myId) a += 'ByYou'; + else if(peerIds[1] === myId) a += 'ForYou'; indexOfAndSplice(peerIds, myId); langPackKey = a as LangPackKey; args = peerIds.map(peerId => getNameDivHTML(peerId, plain)); + args.push(this.wrapJoinVoiceChatAnchor(message as any)); break; } diff --git a/src/lib/appManagers/appUsersManager.ts b/src/lib/appManagers/appUsersManager.ts index c855025c..4b5b694b 100644 --- a/src/lib/appManagers/appUsersManager.ts +++ b/src/lib/appManagers/appUsersManager.ts @@ -14,7 +14,7 @@ import { filterUnique, indexOfAndSplice } from "../../helpers/array"; import { CancellablePromise, deferredPromise } from "../../helpers/cancellablePromise"; import cleanSearchText from "../../helpers/cleanSearchText"; import cleanUsername from "../../helpers/cleanUsername"; -import { tsNow } from "../../helpers/date"; +import { formatFullSentTimeRaw, tsNow } from "../../helpers/date"; import { formatPhoneNumber } from "../../helpers/formatPhoneNumber"; import { safeReplaceObject, isObject } from "../../helpers/object"; import { Chat, InputContact, InputMedia, InputPeer, InputUser, User as MTUser, UserProfilePhoto, UserStatus } from "../../layer"; @@ -581,23 +581,24 @@ export class AppUsersManager { case 'userStatusOffline': { const date = user.status.was_online; - const now = Date.now() / 1000; + const today = new Date(); + const now = today.getTime() / 1000 | 0; - if((now - date) < 60) { + const diff = now - date; + if(diff < 60) { key = 'Peer.Status.justNow'; - } else if((now - date) < 3600) { + } else if(diff < 3600) { key = 'Peer.Status.minAgo'; - const c = (now - date) / 60 | 0; + const c = diff / 60 | 0; args = [c]; - } else if(now - date < 86400) { + } else if(diff < 86400 && today.getDate() === new Date(date * 1000).getDate()) { key = 'LastSeen.HoursAgo'; - const c = (now - date) / 3600 | 0; + const c = diff / 3600 | 0; args = [c]; } else { key = 'Peer.Status.LastSeenAt'; - const d = new Date(date * 1000); - args = [('0' + d.getDate()).slice(-2) + '.' + ('0' + (d.getMonth() + 1)).slice(-2), - ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2)]; + const {dateEl, timeEl} = formatFullSentTimeRaw(date); + args = [dateEl, timeEl]; } break; diff --git a/src/lib/mtproto/apiManager.ts b/src/lib/mtproto/apiManager.ts index 02c9c923..a1ed18a2 100644 --- a/src/lib/mtproto/apiManager.ts +++ b/src/lib/mtproto/apiManager.ts @@ -30,11 +30,16 @@ import IDBStorage from '../idb'; import CryptoWorker from "../crypto/cryptoworker"; import ctx from '../../environment/ctx'; import noop from '../../helpers/noop'; +import Modes from '../../config/modes'; /// #if !MTPROTO_WORKER import rootScope from '../rootScope'; /// #endif +/// #if MTPROTO_AUTO +import transportController from './transports/controller'; +/// #endif + /* var networker = apiManager.cachedNetworkers.websocket.upload[2]; networker.wrapMtpMessage({ _: 'msgs_state_req', @@ -73,25 +78,45 @@ export class ApiManager { private cachedNetworkers: { [transportType in TransportType]: { [connectionType in ConnectionType]: { - [dcId: number]: MTPNetworker[] + [dcId: DcId]: MTPNetworker[] } } - } = {} as any; + }; - private cachedExportPromise: {[x: number]: Promise} = {}; - private gettingNetworkers: {[dcIdAndType: string]: Promise} = {}; - private baseDcId: DcId = 0 as DcId; + private cachedExportPromise: {[x: number]: Promise}; + private gettingNetworkers: {[dcIdAndType: string]: Promise}; + private baseDcId: DcId; //public telegramMeNotified = false; - private log: ReturnType = logger('API'); + private log: ReturnType; private afterMessageTempIds: { [tempId: string]: { messageId: string, promise: Promise } - } = {}; + }; + + private transportType: TransportType; + + constructor() { + this.log = logger('API'); + + this.cachedNetworkers = {} as any; + this.cachedExportPromise = {}; + this.gettingNetworkers = {}; + this.baseDcId = 0; + this.afterMessageTempIds = {}; + + this.transportType = Modes.transport; + + /// #if MTPROTO_AUTO + transportController.addEventListener('transport', (transportType) => { + this.changeTransportType(transportType); + }); + /// #endif + } //private lol = false; @@ -112,6 +137,67 @@ export class ApiManager { } } */ + private getTransportType(connectionType: ConnectionType) { + /// #if MTPROTO_HTTP_UPLOAD + // @ts-ignore + const transportType: TransportType = connectionType === 'upload' && IS_SAFARI ? 'https' : 'websocket'; + //const transportType: TransportType = connectionType !== 'client' ? 'https' : 'websocket'; + /// #else + // @ts-ignore + const transportType: TransportType = this.transportType; + /// #endif + + return transportType; + } + + private iterateNetworkers(callback: (o: {networker: MTPNetworker, dcId: DcId, connectionType: ConnectionType, transportType: TransportType, index: number, array: MTPNetworker[]}) => void) { + for(const transportType in this.cachedNetworkers) { + const connections = this.cachedNetworkers[transportType as TransportType]; + for(const connectionType in connections) { + const dcs = connections[connectionType as ConnectionType]; + for(const dcId in dcs) { + const networkers = dcs[dcId as any as DcId]; + networkers.forEach((networker, idx, arr) => { + callback({ + networker, + dcId: +dcId as DcId, + connectionType: connectionType as ConnectionType, + transportType: transportType as TransportType, + index: idx, + array: arr + }); + }); + } + } + } + } + + private chooseServer(dcId: DcId, connectionType: ConnectionType, transportType: TransportType) { + return dcConfigurator.chooseServer(dcId, connectionType, transportType, connectionType === 'client'); + } + + public changeTransportType(transportType: TransportType) { + const oldTransportType = this.transportType; + if(oldTransportType === transportType) { + return; + } + + this.log('changing transport from', oldTransportType, 'to', transportType); + + const oldObject = this.cachedNetworkers[oldTransportType]; + const newObject = this.cachedNetworkers[transportType]; + this.cachedNetworkers[transportType] = oldObject; + this.cachedNetworkers[oldTransportType] = newObject; + + this.transportType = transportType; + + this.iterateNetworkers((info) => { + const transportType = this.getTransportType(info.connectionType); + const transport = this.chooseServer(info.dcId, info.connectionType, transportType); + info.networker.changeTransport(transport); + }); + } + public async getBaseDcId() { if(this.baseDcId) { return this.baseDcId; @@ -203,16 +289,8 @@ export class ApiManager { const connectionType: ConnectionType = options.fileDownload ? 'download' : (options.fileUpload ? 'upload' : 'client'); //const connectionType: ConnectionType = 'client'; - /// #if MTPROTO_HTTP_UPLOAD - // @ts-ignore - const transportType: TransportType = connectionType === 'upload' && IS_SAFARI ? 'https' : 'websocket'; - //const transportType: TransportType = connectionType !== 'client' ? 'https' : 'websocket'; - /// #else - // @ts-ignore - const transportType = 'websocket'; - /// #endif - - if(!this.cachedNetworkers.hasOwnProperty(transportType)) { + const transportType = this.getTransportType(connectionType); + if(!this.cachedNetworkers[transportType]) { this.cachedNetworkers[transportType] = { client: {}, download: {}, @@ -250,9 +328,9 @@ export class ApiManager { const ak: DcAuthKey = `dc${dcId}_auth_key` as any; const ss: DcServerSalt = `dc${dcId}_server_salt` as any; + let transport = this.chooseServer(dcId, connectionType, transportType); return this.gettingNetworkers[getKey] = Promise.all([ak, ss].map(key => sessionStorage.get(key))) .then(async([authKeyHex, serverSaltHex]) => { - const transport = dcConfigurator.chooseServer(dcId, connectionType, transportType, connectionType === 'client'); let networker: MTPNetworker; if(authKeyHex && authKeyHex.length === 512) { if(!serverSaltHex || serverSaltHex.length !== 16) { @@ -263,19 +341,17 @@ export class ApiManager { const authKeyId = (await CryptoWorker.invokeCrypto('sha1-hash', authKey)).slice(-8); const serverSalt = bytesFromHex(serverSaltHex); - networker = networkerFactory.getNetworker(dcId, authKey, authKeyId, serverSalt, transport, options); + networker = networkerFactory.getNetworker(dcId, authKey, authKeyId, serverSalt, options); } else { try { // if no saved state const auth = await authorizer.auth(dcId); - const storeObj = { + sessionStorage.set({ [ak]: bytesToHex(auth.authKey), [ss]: bytesToHex(auth.serverSalt) - }; - - sessionStorage.set(storeObj); + }); - networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, transport, options); + networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, options); } catch(error) { this.log('Get networker error', error, (error as Error).stack); delete this.gettingNetworkers[getKey]; @@ -283,6 +359,16 @@ export class ApiManager { } } + // ! cannot get it before this promise because simultaneous changeTransport will change nothing + const newTransportType = this.getTransportType(connectionType); + if(newTransportType !== transportType) { + transport.destroy(); + DcConfigurator.removeTransport(dcConfigurator.chosenServers, transport); + transport = this.chooseServer(dcId, connectionType, newTransportType); + } + + networker.changeTransport(transport); + /* networker.onConnectionStatusChange = (online) => { console.log('status:', online); }; */ @@ -315,7 +401,6 @@ export class ApiManager { networker.destroy(); networkerFactory.removeNetworker(networker); DcConfigurator.removeTransport(this.cachedNetworkers, networker); - DcConfigurator.removeTransport(dcConfigurator.chosenServers, networker.transport); }; networker.setDrainTimeout(); diff --git a/src/lib/mtproto/authorizer.ts b/src/lib/mtproto/authorizer.ts index 9a0b85a5..320cb8a1 100644 --- a/src/lib/mtproto/authorizer.ts +++ b/src/lib/mtproto/authorizer.ts @@ -10,7 +10,7 @@ */ import { TLSerialization, TLDeserialization } from "./tl_utils"; -import dcConfigurator from "./dcConfigurator"; +import dcConfigurator, { TransportType } from "./dcConfigurator"; import rsaKeysManager from "./rsaKeysManager"; import timeManager from "./timeManager"; @@ -21,7 +21,12 @@ import { bytesCmp, bytesToHex, bytesFromHex, bytesXor } from "../../helpers/byte import DEBUG from "../../config/debug"; import { cmp, int2bigInt, one, pow, str2bigInt, sub } from "../../vendor/leemon"; import { addPadding } from "./bin_utils"; -import { Awaited } from "../../types"; +import { Awaited, DcId } from "../../types"; +import { ApiError } from "./apiManager"; + +/// #if MTPROTO_AUTO +import transportController from "./transports/controller"; +/// #endif /* let fNewNonce: any = bytesFromHex('8761970c24cb2329b5b2459752c502f3057cb7e8dbab200e526e8767fdc73b3c').reverse(); let fNonce: any = bytesFromHex('b597720d11faa5914ef485c529cde414').reverse(); @@ -66,8 +71,6 @@ type AuthOptions = { localTime?: number, serverTime?: any, - - localTry?: number }; type ResPQ = { @@ -100,16 +103,23 @@ type req_DH_params = { export class Authorizer { private cached: { - [dcId: number]: Promise - } = {}; + [dcId: DcId]: Promise + }; private log: ReturnType; + + private transportType: TransportType; + + /// #if MTPROTO_AUTO + private getTransportTypePromise: Promise; + /// #endif constructor() { + this.cached = {}; this.log = logger(`AUTHORIZER`, LogTypes.Error | LogTypes.Log); } - private sendPlainRequest(dcId: number, requestArray: Uint8Array) { + private sendPlainRequest(dcId: DcId, requestArray: Uint8Array) { const requestLength = requestArray.byteLength; const header = new TLSerialization(); @@ -122,7 +132,7 @@ export class Authorizer { resultArray.set(headerArray); resultArray.set(requestArray, headerArray.length); - const transport = dcConfigurator.chooseServer(dcId); + const transport = dcConfigurator.chooseServer(dcId, 'client', this.transportType); const baseError = { code: 406, type: 'NETWORK_BAD_RESPONSE' @@ -570,40 +580,48 @@ export class Authorizer { } } } - - public async auth(dcId: number): Promise { - if(dcId in this.cached) { - return this.cached[dcId]; - } - - const nonce = /* fNonce ? fNonce : */new Uint8Array(16).randomize(); - /* const nonce = new Array(16); - MTProto.secureRandom.nextBytes(nonce); */ - - if(!dcConfigurator.chooseServer(dcId)) { - throw new Error('[MT] No server found for dc ' + dcId); - } - // await new Promise((resolve) => setTimeout(resolve, 2e3)); - - const auth: AuthOptions = {dcId, nonce, localTry: 1}; - - try { - const promise = this.sendReqPQ(auth); - this.cached[dcId] = promise; - return await promise; - } catch(err) { - if(/* err.originalError === -404 && */auth.localTry <= 3) { - return this.sendReqPQ({ - dcId: auth.dcId, - nonce: new Uint8Array(16).randomize(), - localTry: auth.localTry + 1 - }); + /// #if MTPROTO_AUTO + private getTransportType() { + if(this.getTransportTypePromise) return this.getTransportTypePromise; + return this.getTransportTypePromise = transportController.pingTransports().then(({websocket}) => { + this.transportType = websocket ? 'websocket' : 'https'; + }); + } + /// #endif + + public auth(dcId: DcId) { + let promise = this.cached[dcId]; + if(promise) { + return promise; + } + + promise = new Promise(async(resolve, reject) => { + /// #if MTPROTO_AUTO + await this.getTransportType(); + /// #endif + + let error: ApiError; + let _try = 1; + while(_try++ <= 3) { + try { + const auth: AuthOptions = { + dcId, + nonce: new Uint8Array(16).randomize() + }; + + const promise = this.sendReqPQ(auth); + resolve(await promise); + return; + } catch(err) { + error = err; + } } - delete this.cached[dcId]; - throw err; - } + reject(error); + }); + + return this.cached[dcId] = promise; } } diff --git a/src/lib/mtproto/dcConfigurator.ts b/src/lib/mtproto/dcConfigurator.ts index 9e509031..14d0bd8e 100644 --- a/src/lib/mtproto/dcConfigurator.ts +++ b/src/lib/mtproto/dcConfigurator.ts @@ -11,18 +11,20 @@ import MTTransport, { MTConnectionConstructable } from './transports/transport'; import Modes from '../../config/modes'; +import { indexOfAndSplice } from '../../helpers/array'; +import App from '../../config/app'; -/// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD +/// #if MTPROTO_HAS_HTTP import HTTP from './transports/http'; /// #endif -/// #if !MTPROTO_HTTP +/// #if MTPROTO_HAS_WS import Socket from './transports/websocket'; import TcpObfuscated from './transports/tcpObfuscated'; import { IS_SAFARI } from '../../environment/userAgent'; import { IS_WEB_WORKER } from '../../helpers/context'; import SocketProxied from './transports/socketProxied'; -import App from '../../config/app'; +import { DcId } from '../../types'; /// #endif export type TransportType = 'websocket' | 'https' | 'http'; @@ -30,7 +32,7 @@ export type ConnectionType = 'client' | 'download' | 'upload'; type Servers = { [transportType in TransportType]: { [connectionType in ConnectionType]: { - [dcId: number]: MTTransport[] + [dcId: DcId]: MTTransport[] } } }; @@ -56,8 +58,8 @@ export class DcConfigurator { public chosenServers: Servers = {} as any; - /// #if !MTPROTO_HTTP - private transportSocket = (dcId: number, connectionType: ConnectionType, suffix: string) => { + /// #if MTPROTO_HAS_WS + private transportSocket = (dcId: DcId, connectionType: ConnectionType, suffix: string) => { const path = 'apiws' + TEST_SUFFIX; const chosenServer = `wss://${App.suffix.toLowerCase()}ws${dcId}${suffix}.web.telegram.org/${path}`; const logSuffix = connectionType === 'upload' ? '-U' : connectionType === 'download' ? '-D' : ''; @@ -70,25 +72,33 @@ export class DcConfigurator { }; /// #endif - /// #if MTPROTO_HTTP_UPLOAD || MTPROTO_HTTP - private transportHTTP = (dcId: number, connectionType: ConnectionType, suffix: string) => { + /// #if MTPROTO_HAS_HTTP + private transportHTTP = (dcId: DcId, connectionType: ConnectionType, suffix: string) => { + let chosenServer: string; if(Modes.ssl || !Modes.http) { const subdomain = this.sslSubdomains[dcId - 1] + (connectionType !== 'client' ? '-1' : ''); const path = Modes.test ? 'apiw_test1' : 'apiw1'; - const chosenServer = 'https://' + subdomain + '.web.telegram.org/' + path; - return new HTTP(dcId, chosenServer); + chosenServer = 'https://' + subdomain + '.web.telegram.org/' + path; } else { for(let dcOption of this.dcOptions) { if(dcOption.id === dcId) { - const chosenServer = 'http://' + dcOption.host + (dcOption.port !== 80 ? ':' + dcOption.port : '') + '/apiw1'; - return new HTTP(dcId, chosenServer); + chosenServer = 'http://' + dcOption.host + (dcOption.port !== 80 ? ':' + dcOption.port : '') + '/apiw1'; + break; } } } + + const logSuffix = connectionType === 'upload' ? '-U' : connectionType === 'download' ? '-D' : ''; + return new HTTP(dcId, chosenServer, logSuffix); }; /// #endif - public chooseServer(dcId: number, connectionType: ConnectionType = 'client', transportType: TransportType = 'websocket', reuse = true) { + public chooseServer( + dcId: DcId, + connectionType: ConnectionType = 'client', + transportType: TransportType = Modes.transport, + reuse = true + ) { /* if(transportType === 'websocket' && !Modes.multipleConnections) { connectionType = 'client'; } */ @@ -114,7 +124,7 @@ export class DcConfigurator { const suffix = connectionType === 'client' ? '' : '-1'; - /// #if MTPROTO_HTTP_UPLOAD + /// #if MTPROTO_HAS_WS && MTPROTO_HAS_HTTP transport = (transportType === 'websocket' ? this.transportSocket : this.transportHTTP)(dcId, connectionType, suffix); /// #elif !MTPROTO_HTTP transport = this.transportSocket(dcId, connectionType, suffix); @@ -145,10 +155,7 @@ export class DcConfigurator { for(const dcId in obj[transportType][connectionType]) { // @ts-ignore const transports: T[] = obj[transportType][connectionType][dcId]; - const idx = transports.indexOf(transport); - if(idx !== -1) { - transports.splice(idx, 1); - } + indexOfAndSplice(transports, transport); } } } diff --git a/src/lib/mtproto/networker.ts b/src/lib/mtproto/networker.ts index b2770e6f..ddceb3b0 100644 --- a/src/lib/mtproto/networker.ts +++ b/src/lib/mtproto/networker.ts @@ -27,7 +27,7 @@ import DEBUG from '../../config/debug'; import Modes from '../../config/modes'; import noop from '../../helpers/noop'; -/// #if MTPROTO_HTTP_UPLOAD || MTPROTO_HTTP +/// #if MTPROTO_HAS_HTTP import HTTP from './transports/http'; /// #endif @@ -35,6 +35,8 @@ import type TcpObfuscated from './transports/tcpObfuscated'; import { bigInt2str, rightShift_, str2bigInt } from '../../vendor/leemon'; import { forEachReverse } from '../../helpers/array'; import { ConnectionStatus } from './connectionStatus'; +import ctx from '../../environment/ctx'; +import dcConfigurator, { DcConfigurator } from './dcConfigurator'; //console.error('networker included!', new Error().stack); @@ -95,21 +97,23 @@ export default class MTPNetworker { private pendingMessages: {[msgId: MTLong]: number} = {}; private pendingAcks: Array = []; private pendingResends: Array = []; - public connectionInited = false; + public connectionInited: boolean; private nextReqTimeout: number; private nextReq: number = 0; - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD - //private longPollInt: number; - private longPollPending = 0; + /// #if MTPROTO_HAS_HTTP + private longPollInterval: number; + private longPollPending: number; + private checkConnectionRetryAt: number; private checkConnectionTimeout: number; private checkConnectionPeriod = 0; - private sleepAfter = 0; + private sleepAfter: number; private offline = false; + private sendingLongPoll: boolean; /// #endif - private seqNo: number = 0; + private seqNo: number; private prevSessionId: Uint8Array; private sessionId: Uint8Array; private serverSalt: Uint8Array; @@ -133,14 +137,21 @@ export default class MTPNetworker { public onDrain: () => void; private onDrainTimeout: number; + public transport: MTTransport; + //private disconnectDelay: number; //private pingPromise: CancellablePromise; //public onConnectionStatusChange: (online: boolean) => void; //private debugRequests: Array<{before: Uint8Array, after: Uint8Array}> = []; - constructor(public dcId: number, private authKey: Uint8Array, private authKeyId: Uint8Array, - serverSalt: Uint8Array, public transport: MTTransport, options: InvokeApiOptions = {}) { + constructor( + public dcId: number, + private authKey: Uint8Array, + private authKeyId: Uint8Array, + serverSalt: Uint8Array, + options: InvokeApiOptions = {} + ) { this.authKeyUint8 = convertToUint8Array(this.authKey); this.serverSalt = convertToUint8Array(serverSalt); @@ -151,7 +162,7 @@ export default class MTPNetworker { const suffix = this.isFileUpload ? '-U' : this.isFileDownload ? '-D' : ''; this.name = 'NET-' + dcId + suffix; //this.log = logger(this.name, this.upload && this.dcId === 2 ? LogLevels.debug | LogLevels.warn | LogLevels.log | LogLevels.error : LogLevels.error); - this.log = logger(this.name, LogTypes.Log | /* LogTypes.Debug | */LogTypes.Error | LogTypes.Warn, undefined); + this.log = logger(this.name, LogTypes.Log | LogTypes.Debug | LogTypes.Error | LogTypes.Warn, undefined); this.log('constructor'/* , this.authKey, this.authKeyID, this.serverSalt */); // Test resend after bad_server_salt @@ -168,33 +179,12 @@ export default class MTPNetworker { // rootScope.offlineConnecting = true */ // } - /// #if MTPROTO_HTTP_UPLOAD - if(this.transport instanceof HTTP) { - /* this.longPollInt = */setInterval(this.checkLongPoll, 10000); - this.checkLongPoll(); - } else { - (this.transport as TcpObfuscated).networker = this; - } - /// #elif MTPROTO_HTTP - //if(this.transport instanceof HTTP) { - /* this.longPollInt = */setInterval(this.checkLongPoll, 10000); - this.checkLongPoll(); - /// #else - //} else { - (this.transport as TcpObfuscated).networker = this; - //} - /// #endif - // * handle outcoming dead socket, server will close the connection // if((this.transport as TcpObfuscated).networker) { // this.disconnectDelay = /* (this.transport as TcpObfuscated).retryTimeout */75; // //setInterval(this.sendPingDelayDisconnect, (this.disconnectDelay - 5) * 1000); // this.sendPingDelayDisconnect(); // } - - if((this.transport as TcpObfuscated).connected) { - this.setConnectionStatus(ConnectionStatus.Connected); - } } private updateSession() { @@ -232,9 +222,9 @@ export default class MTPNetworker { sentMessage.msg_id = timeManager.generateId(); sentMessage.seq_no = this.generateSeqNo(sentMessage.notContentRelated || sentMessage.container); - /* if(DEBUG) { - this.log('updateSentMessage', sentMessage.msg_id, sentMessageId); - } */ + if(this.debug) { + this.log(`updateSentMessage, old=${sentMessageId}, new=${sentMessage.msg_id}`); + } this.sentMessages[sentMessage.msg_id] = sentMessage; delete this.sentMessages[sentMessageId]; @@ -372,20 +362,75 @@ export default class MTPNetworker { return this.pushMessage(message, options); } + public changeTransport(transport?: MTTransport) { + const oldTransport = this.transport; + if(oldTransport) { + oldTransport.destroy(); + + DcConfigurator.removeTransport(dcConfigurator.chosenServers, this.transport); + + if(this.nextReqTimeout) { + clearTimeout(this.nextReqTimeout); + this.nextReqTimeout = 0; + this.nextReq = 0; + } + + /// #if MTPROTO_HAS_HTTP + if(this.longPollInterval !== undefined) { + clearInterval(this.longPollInterval); + this.longPollInterval = undefined; + } + + if(this.checkConnectionTimeout !== undefined) { + clearTimeout(this.checkConnectionTimeout); + this.checkConnectionTimeout = undefined; + } + /// #endif + } + + this.transport = transport; + if(!transport) { + return; + } + + transport.networker = this; + + /// #if MTPROTO_HAS_HTTP + /// #if MTPROTO_HAS_WS + if(transport instanceof HTTP) { + /// #endif + this.longPollInterval = ctx.setInterval(this.checkLongPoll, 10000); + this.checkLongPoll(); + this.checkConnection('changed transport'); + /// #if MTPROTO_HAS_WS + } + /// #endif + /// #endif + + if(transport.connected && (transport as TcpObfuscated).connection) { + this.setConnectionStatus(ConnectionStatus.Connected); + } + + this.resend(); + } + public destroy() { - //assumeType(this.transport); - (this.transport as TcpObfuscated).destroy(); + this.changeTransport(); } public forceReconnectTimeout() { if((this.transport as TcpObfuscated).reconnect) { (this.transport as TcpObfuscated).reconnect(); + } else { + this.resend(); } } public forceReconnect() { if((this.transport as TcpObfuscated).forceReconnect) { (this.transport as TcpObfuscated).forceReconnect(); + } else { + this.checkConnection('force reconnect'); } } @@ -496,13 +541,14 @@ export default class MTPNetworker { // }); // }; - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD + /// #if MTPROTO_HAS_HTTP private checkLongPoll = () => { const isClean = this.cleanupSent(); //this.log.error('Check lp', this.longPollPending, this.dcId, isClean, this); if((this.longPollPending && Date.now() < this.longPollPending) || this.offline || - this.isStopped()) { + this.isStopped() || + this.isFileNetworker) { //this.log('No lp this time'); return false; } @@ -510,7 +556,6 @@ export default class MTPNetworker { sessionStorage.get('dc').then((baseDcId) => { if(isClean && ( baseDcId !== this.dcId || - this.isFileNetworker || (this.sleepAfter && Date.now() > this.sleepAfter) )) { //console.warn(dT(), 'Send long-poll for DC is delayed', this.dcId, this.sleepAfter); @@ -522,10 +567,12 @@ export default class MTPNetworker { }; private sendLongPoll() { + if(this.sendingLongPoll) return; + this.sendingLongPoll = true; const maxWait = 25000; this.longPollPending = Date.now() + maxWait; - //this.log('Set lp', this.longPollPending, tsNow()) + this.debug && this.log.debug('sendLongPoll', this.longPollPending); this.wrapMtpCall('http_wait', { max_delay: 500, @@ -535,19 +582,19 @@ export default class MTPNetworker { noResponse: true, longPoll: true }).then(() => { - this.longPollPending = 0; + this.longPollPending = undefined; setTimeout(this.checkLongPoll, 0); }, (error: ErrorEvent) => { this.log('Long-poll failed', error); + }).finally(() => { + this.sendingLongPoll = undefined; }); } private checkConnection = (event: Event | string) => { - /* rootScope.offlineConnecting = true */ - - this.log('Check connection', event); + this.debug && this.log('Check connection', event); clearTimeout(this.checkConnectionTimeout); - this.checkConnectionTimeout = 0; + this.checkConnectionTimeout = undefined; const serializer = new TLSerialization({mtproto: true}); const pingId = randomLong(); @@ -562,108 +609,96 @@ export default class MTPNetworker { body: serializer.getBytes(true) }; - this.sendEncryptedRequest(pingMessage).then((result) => { - /* delete rootScope.offlineConnecting */ + if(this.offline) { + this.setConnectionStatus(ConnectionStatus.Connecting); + } + + this.sendEncryptedRequest(pingMessage).then(() => { this.toggleOffline(false); }, () => { - this.log('Delay ', this.checkConnectionPeriod * 1000); - this.checkConnectionTimeout = setTimeout(this.checkConnection, this.checkConnectionPeriod * 1000 | 0); + this.debug && this.log('Delay ', this.checkConnectionPeriod * 1000); + this.checkConnectionTimeout = ctx.setTimeout(this.checkConnection, this.checkConnectionPeriod * 1000 | 0); this.checkConnectionPeriod = Math.min(60, this.checkConnectionPeriod * 1.5); - /* setTimeout(function() { - delete rootScope.offlineConnecting - }, 1000); */ }); }; - private toggleOffline(enabled: boolean) { - // this.log('toggle ', enabled, this.dcId, this.iii) - if(this.offline !== undefined && this.offline === enabled) { - return false; - } - - this.offline = enabled; - /* rootScope.offline = enabled; - rootScope.offlineConnecting = false; */ + private toggleOffline(offline: boolean) { + if(this.offline !== offline) { + this.offline = offline; - if(this.offline) { - clearTimeout(this.nextReqTimeout); - this.nextReqTimeout = 0; - this.nextReq = 0; - - if(this.checkConnectionPeriod < 1.5) { - this.checkConnectionPeriod = 0; - } - - this.checkConnectionTimeout = setTimeout(this.checkConnection, this.checkConnectionPeriod * 1000 | 0); - this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5); - - /// #if !MTPROTO_WORKER - document.body.addEventListener('online', this.checkConnection, false); - document.body.addEventListener('focus', this.checkConnection, false); - /// #endif - } else { - this.checkLongPoll(); + if(offline) { + clearTimeout(this.nextReqTimeout); + this.nextReqTimeout = 0; + this.nextReq = 0; + + if(this.checkConnectionPeriod < 1.5) { + this.checkConnectionPeriod = 0; + } + + const delay = this.checkConnectionPeriod * 1000 | 0; + this.checkConnectionRetryAt = Date.now() + delay; + this.setConnectionStatus(ConnectionStatus.Closed, this.checkConnectionRetryAt); + this.checkConnectionTimeout = ctx.setTimeout(this.checkConnection, delay); + this.checkConnectionPeriod = Math.min(30, (1 + this.checkConnectionPeriod) * 1.5); + + /// #if !MTPROTO_WORKER + document.body.addEventListener('online', this.checkConnection, false); + document.body.addEventListener('focus', this.checkConnection, false); + /// #endif + } else { + this.setConnectionStatus(ConnectionStatus.Connected); + this.checkLongPoll(); - this.scheduleRequest(); - - /// #if !MTPROTO_WORKER - document.body.removeEventListener('online', this.checkConnection); - document.body.removeEventListener('focus', this.checkConnection); - /// #endif + this.scheduleRequest(); + + /// #if !MTPROTO_WORKER + document.body.removeEventListener('online', this.checkConnection); + document.body.removeEventListener('focus', this.checkConnection); + /// #endif - clearTimeout(this.checkConnectionTimeout); - this.checkConnectionTimeout = 0; + clearTimeout(this.checkConnectionTimeout); + this.checkConnectionTimeout = undefined; + } } - + + this.setConnectionStatus(offline ? ConnectionStatus.Closed : ConnectionStatus.Connected, offline ? this.checkConnectionRetryAt : undefined); } private handleSentEncryptedRequestHTTP(promise: ReturnType, message: MTMessage, noResponseMsgs: string[]) { - promise - .then((result) => { + // let timeout = setTimeout(() => { + // this.log.error('handleSentEncryptedRequestHTTP timeout', promise, message, noResponseMsgs); + // }, 5e3); + + promise.then((result) => { this.toggleOffline(false); - // this.log('parse for', message) - this.parseResponse(result).then((response) => { - if(Modes.debug) { - this.log.debug('Server response', response); - } + // this.log('parse for', message); + return this.parseResponse(result).then((response) => { + this.debug && this.log.debug('Server response', response); this.processMessage(response.response, response.messageId, response.sessionId); - - noResponseMsgs.forEach((msgId) => { - if(this.sentMessages[msgId]) { - const deferred = this.sentMessages[msgId].deferred; - delete this.sentMessages[msgId]; - deferred.resolve(); - } - }); this.checkLongPoll(); - this.checkConnectionPeriod = Math.max(1.1, Math.sqrt(this.checkConnectionPeriod)); + + return true; }); }, (error) => { this.log.error('Encrypted request failed', error, message); - if(message.container) { - message.inner.forEach((msgId: string) => { - this.pendingMessages[msgId] = 0; - }); + this.pushResend(message.msg_id); + this.toggleOffline(true); - delete this.sentMessages[message.msg_id]; - } else { - this.pendingMessages[message.msg_id] = 0; - } - + return false; + }).then((shouldResolve) => { + // clearTimeout(timeout); noResponseMsgs.forEach((msgId) => { if(this.sentMessages[msgId]) { const deferred = this.sentMessages[msgId].deferred; delete this.sentMessages[msgId]; delete this.pendingMessages[msgId]; - deferred.reject(); + shouldResolve ? deferred.resolve() : deferred.reject(); } - }) - - this.toggleOffline(true); + }); }); } /// #endif @@ -732,7 +767,7 @@ export default class MTPNetworker { public setDrainTimeout() { if(!this.activeRequests && this.onDrain && this.onDrainTimeout === undefined) { - this.onDrainTimeout = self.setTimeout(() => { + this.onDrainTimeout = ctx.setTimeout(() => { this.onDrainTimeout = undefined; this.log('drain'); this.onDrain(); @@ -890,7 +925,7 @@ export default class MTPNetworker { //const currentTime = Date.now(); let messagesByteLen = 0; - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD + /// #if MTPROTO_HAS_HTTP let hasApiCall = false; let hasHttpWait = false; /// #endif @@ -923,7 +958,7 @@ export default class MTPNetworker { messages.push(message); messagesByteLen += messageByteLength; - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD + /// #if MTPROTO_HAS_HTTP if(message.isAPI) { hasApiCall = true; } else if(message.longPoll) { @@ -940,10 +975,10 @@ export default class MTPNetworker { //} } - /// #if MTPROTO_HTTP_UPLOAD + /// #if MTPROTO_HAS_HTTP + /// #if MTPROTO_HAS_WS if(this.transport instanceof HTTP) /// #endif - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD if(hasApiCall && !hasHttpWait) { const serializer = new TLSerialization({mtproto: true}); serializer.storeMethod('http_wait', { @@ -965,7 +1000,7 @@ export default class MTPNetworker { return; } - /// #if MTPROTO_HTTP_UPLOAD || MTPROTO_HTTP + /// #if MTPROTO_HAS_HTTP const noResponseMsgs: Array = messages.filter(message => message.noResponse).map(message => message.msg_id); /// #endif @@ -981,20 +1016,20 @@ export default class MTPNetworker { this.pendingAcks = []; const promise = this.sendEncryptedRequest(outMessage); - - /// #if MTPROTO_HTTP_UPLOAD - if(!(this.transport instanceof HTTP)) { - //if(noResponseMsgs.length) this.log.error('noResponseMsgs length!', noResponseMsgs); - this.cleanupSent(); // ! WARNING - } else { - this.handleSentEncryptedRequestHTTP(promise, outMessage, noResponseMsgs); - } - /// #elif !MTPROTO_HTTP - this.cleanupSent(); // ! WARNING - /// #else + + /// #if MTPROTO_HAS_HTTP + /// #if MTPROTO_HAS_WS + if(this.transport instanceof HTTP) + /// #endif this.handleSentEncryptedRequestHTTP(promise, outMessage, noResponseMsgs); - //} - /// #endif + /// #endif + + /// #if MTPROTO_HAS_WS + /// #if MTPROTO_HAS_HTTP + if(!(this.transport instanceof HTTP)) + /// #endif + this.cleanupSent(); // ! WARNING + /// #endif if(lengthOverflow) { this.scheduleRequest(); @@ -1154,33 +1189,40 @@ export default class MTPNetworker { private sendEncryptedRequest(message: MTMessage) { return this.getEncryptedOutput(message).then(requestData => { - this.debug && this.log.debug('sendEncryptedRequest: launching message into space:', message, [message.msg_id].concat(message.inner || [])); - + this.debug && this.log.debug('sendEncryptedRequest: launching message into space:', message, [message.msg_id].concat(message.inner || []), requestData.length); const promise: Promise = this.transport.send(requestData) as any; - /// #if !MTPROTO_HTTP && !MTPROTO_HTTP_UPLOAD + // this.debug && this.log.debug('sendEncryptedRequest: launched message into space:', message, promise); + + /// #if !MTPROTO_HAS_HTTP return promise; /// #else + + /// #if MTPROTO_HAS_WS if(!(this.transport instanceof HTTP)) return promise; - + /// #endif + const baseError = { code: 406, type: 'NETWORK_BAD_RESPONSE', transport: this.transport }; + return promise.then((result) => { - if(!result || !result.byteLength) { - return Promise.reject(baseError); + if(!result?.byteLength) { + throw baseError; } - + + // this.debug && this.log.debug('sendEncryptedRequest: got response for:', message, [message.msg_id].concat(message.inner || [])); return result; }, (error) => { if(!error.message && !error.type) { error = Object.assign(baseError, { - type: 'NETWORK_BAD_REQUEST', - originalError: error + type: 'NETWORK_BAD_REQUEST', + originalError: error }); } - return Promise.reject(error); + + throw error; }); /// #endif }); @@ -1322,14 +1364,19 @@ export default class MTPNetworker { return; } */ - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD - if(!(this.transport instanceof HTTP)) { - this.performScheduledRequest(); - return; - } else if(this.offline) { - this.checkConnection('forced schedule'); + /// #if MTPROTO_HAS_HTTP + /// #if MTPROTO_HAS_WS + if(this.transport instanceof HTTP) { + /// #endif + if(this.offline) { + this.checkConnection('forced schedule'); + } + + delay ||= 0; // set zero timeout to pack other messages too + /// #if MTPROTO_HAS_WS } /// #endif + /// #endif const nextReq = Date.now() + (delay || 0); if(this.nextReq && (delay === undefined || this.nextReq <= nextReq)) { @@ -1354,23 +1401,22 @@ export default class MTPNetworker { this.nextReqTimeout = 0; this.nextReq = 0; - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD + /// #if MTPROTO_HAS_HTTP + /// #if MTPROTO_HAS_WS + if(this.transport instanceof HTTP) + /// #endif if(this.offline) { //this.log('Cancel scheduled'); return; } - /// #else - /* if(!this.isOnline) { - return; - } */ + /// #endif this.performScheduledRequest(); - /// #endif }; this.nextReq = nextReq; - if(delay) { + if(delay !== undefined) { this.nextReqTimeout = self.setTimeout(cb, delay); } else { cb(); @@ -1381,11 +1427,16 @@ export default class MTPNetworker { // this.log('ack message', msgID) this.pendingAcks.push(msgId); - /// #if MTPROTO_HTTP || MTPROTO_HTTP_UPLOAD - this.scheduleRequest(30000); - /// #else - this.scheduleRequest(); + let delay: number; + + /// #if MTPROTO_HAS_HTTP + /// #if MTPROTO_HAS_WS + if(this.transport instanceof HTTP) /// #endif + delay = 30000; + /// #endif + + this.scheduleRequest(delay); } private reqResendMessage(msgId: MTLong) { @@ -1680,15 +1731,13 @@ export default class MTPNetworker { break; } - case 'pong': { // * https://core.telegram.org/mtproto/service_messages#ping-messages-pingpong - These messages doesn't require acknowledgments - if((this.transport as TcpObfuscated).networker) { - const sentMessageId = message.msg_id; - const sentMessage = this.sentMessages[sentMessageId]; + case 'pong': { // * https://core.telegram.org/mtproto/service_messages#ping-messages-pingpong - These messages don't require acknowledgments + const sentMessageId = message.msg_id; + const sentMessage = this.sentMessages[sentMessageId]; - if(sentMessage) { - sentMessage.deferred.resolve(message); - delete this.sentMessages[sentMessageId]; - } + if(sentMessage) { + sentMessage.deferred.resolve(message); + delete this.sentMessages[sentMessageId]; } break; diff --git a/src/lib/mtproto/networkerFactory.ts b/src/lib/mtproto/networkerFactory.ts index e6d965d5..f2d498b6 100644 --- a/src/lib/mtproto/networkerFactory.ts +++ b/src/lib/mtproto/networkerFactory.ts @@ -12,9 +12,9 @@ import type { ConnectionStatusChange } from "./connectionStatus"; import MTPNetworker from "./networker"; import { InvokeApiOptions } from "../../types"; -import MTTransport from "./transports/transport"; import App from "../../config/app"; import { MOUNT_CLASS_TO } from "../../config/debug"; +import { indexOfAndSplice } from "../../helpers/array"; export class NetworkerFactory { private networkers: MTPNetworker[] = []; @@ -25,19 +25,16 @@ export class NetworkerFactory { public userAgent = navigator.userAgent; public removeNetworker(networker: MTPNetworker) { - const idx = this.networkers.indexOf(networker); - if(idx !== -1) { - this.networkers.splice(idx, 1); - } + indexOfAndSplice(this.networkers, networker); } public setUpdatesProcessor(callback: (obj: any) => void) { this.updatesProcessor = callback; } - public getNetworker(dcId: number, authKey: Uint8Array, authKeyId: Uint8Array, serverSalt: Uint8Array, transport: MTTransport, options: InvokeApiOptions) { + public getNetworker(dcId: number, authKey: Uint8Array, authKeyId: Uint8Array, serverSalt: Uint8Array, options: InvokeApiOptions) { //console.log('NetworkerFactory: creating new instance of MTPNetworker:', dcId, options); - const networker = new MTPNetworker(dcId, authKey, authKeyId, serverSalt, transport, options); + const networker = new MTPNetworker(dcId, authKey, authKeyId, serverSalt, options); this.networkers.push(networker); return networker; } diff --git a/src/lib/mtproto/transports/controller.ts b/src/lib/mtproto/transports/controller.ts new file mode 100644 index 00000000..d4bed753 --- /dev/null +++ b/src/lib/mtproto/transports/controller.ts @@ -0,0 +1,113 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import App from "../../../config/app"; +import { deferredPromise } from "../../../helpers/cancellablePromise"; +import EventListenerBase from "../../../helpers/eventListenerBase"; +import { pause } from "../../../helpers/schedulers/pause"; +import dcConfigurator, { TransportType } from "../dcConfigurator"; +import type HTTP from "./http"; +import type TcpObfuscated from "./tcpObfuscated"; +import MTTransport from "./transport"; + +export class MTTransportController extends EventListenerBase<{ + change: (opened: MTTransportController['opened']) => void, + transport: (type: TransportType) => void +}> { + private opened: Map; + private transports: {[k in TransportType]?: MTTransport}; + private pinging: boolean; + + constructor() { + super(true); + + this.opened = new Map(); + /* this.addEventListener('change', (opened) => { + this.dispatchEvent('transport', opened.get('websocket') || !opened.get('https') ? 'websocket' : 'https'); + }); */ + + this.addEventListener('change', (opened) => { + if(!opened.get('websocket')) { + this.waitForWebSocket(); + } + }); + + setTimeout(() => { + this.waitForWebSocket(); + }, 0); + } + + public async pingTransports() { + const timeout = 2000; + const transports: {[k in TransportType]?: MTTransport} = this.transports = { + https: dcConfigurator.chooseServer(App.baseDcId, 'client', 'https', false), + websocket: dcConfigurator.chooseServer(App.baseDcId, 'client', 'websocket', false) + }; + + const httpPromise = deferredPromise(); + ((this.transports.https as HTTP)._send(new Uint8Array(), 'no-cors') as any as Promise) + .then(() => httpPromise.resolve(true), () => httpPromise.resolve(false)); + setTimeout(() => httpPromise.resolve(false), timeout); + + const websocketPromise = deferredPromise(); + const socket = transports.websocket as TcpObfuscated; + socket.setAutoReconnect(false); + socket.connection.addEventListener('close', () => websocketPromise.resolve(false), {once: true}); + socket.connection.addEventListener('open', () => websocketPromise.resolve(true), {once: true}); + setTimeout(() => websocketPromise.resolve(false), timeout); + + const [isHttpAvailable, isWebSocketAvailable] = await Promise.all([httpPromise, websocketPromise]); + + for(const transportType in transports) { + const transport = transports[transportType as TransportType]; + transport.destroy(); + } + + return { + https: isHttpAvailable || this.opened.get('https') > 0, + websocket: isWebSocketAvailable || this.opened.get('websocket') > 0 + }; + } + + public async waitForWebSocket() { + if(this.pinging) return; + this.pinging = true; + + while(true) { + const {https, websocket} = await this.pingTransports(); + if(https || websocket) { + this.dispatchEvent('transport', websocket || !https ? 'websocket' : 'https'); + } + + if(websocket) { + break; + } + + await pause(10000); + } + + this.pinging = false; + } + + public setTransportValue(type: TransportType, value: boolean) { + let length = this.opened.get(type) || 0; + length += value ? 1 : -1; + + this.opened.set(type, length); + this.dispatchEvent('change', this.opened); + } + + public setTransportOpened(type: TransportType) { + return this.setTransportValue(type, true); + } + + public setTransportClosed(type: TransportType) { + return this.setTransportValue(type, false); + } +} + +const transportController = new MTTransportController(); +export default transportController; diff --git a/src/lib/mtproto/transports/http.ts b/src/lib/mtproto/transports/http.ts index 888319af..b6e6802f 100644 --- a/src/lib/mtproto/transports/http.ts +++ b/src/lib/mtproto/transports/http.ts @@ -4,28 +4,127 @@ * https://github.com/morethanwords/tweb/blob/master/LICENSE */ +import { pause } from '../../../helpers/schedulers/pause'; +import { DcId } from '../../../types'; +import { logger, LogTypes } from '../../logger'; +import type MTPNetworker from '../networker'; import MTTransport from './transport'; +import Modes from '../../../config/modes'; + +/// #if MTPROTO_AUTO +import transportController from './controller'; +/// #endif export default class HTTP implements MTTransport { - constructor(protected dcId: number, protected url: string) { + public networker: MTPNetworker; + private log: ReturnType; + + private pending: Array<{ + resolve: (body: Uint8Array) => void, + reject: any, + body: Uint8Array + }> = []; + private releasing: boolean; + + public connected: boolean; + private destroyed: boolean; + private debug: boolean; + + constructor(protected dcId: DcId, protected url: string, logSuffix: string) { + this.debug = Modes.debug && false; + + let logTypes = LogTypes.Error | LogTypes.Log; + if(this.debug) logTypes |= LogTypes.Debug; + + this.log = logger(`HTTP-${dcId}` + logSuffix, logTypes); + this.log('constructor'); + + this.connected = false; } - public send(data: Uint8Array) { - return fetch(this.url, {method: 'POST', body: data}).then(response => { - //console.log('http response', response/* , response.arrayBuffer() */); + public _send(body: Uint8Array, mode?: RequestMode) { + this.debug && this.log.debug('-> body length to send:', body.length); - if(response.status !== 200) { + return fetch(this.url, {method: 'POST', body, mode}).then(response => { + if(response.status !== 200 && !mode) { response.arrayBuffer().then(buffer => { - console.log('not 200', - new TextDecoder("utf-8").decode(new Uint8Array(buffer))); - }) + this.log.error('not 200', + new TextDecoder("utf-8").decode(new Uint8Array(buffer))); + }); throw response; - } + } + + this.setConnected(true); + + // * test resending by dropping random request + // if(Math.random() > .5) { + // throw 'asd'; + // } return response.arrayBuffer().then(buffer => { return new Uint8Array(buffer); }); + }, (err) => { + this.setConnected(false); + throw err; }); } + + private setConnected(connected: boolean) { + if(this.connected === connected || this.destroyed) { + return; + } + + this.connected = connected; + + /// #if MTPROTO_AUTO + transportController.setTransportValue('https', connected); + /// #endif + } + + public destroy() { + this.setConnected(false); + this.destroyed = true; + this.pending.forEach(pending => pending.reject()); + this.pending.length = 0; + } + + public send(body: Uint8Array) { + if(this.networker) { + return this._send(body); + } else { + const promise = new Promise((resolve, reject) => { + this.pending.push({resolve, reject, body}); + }); + + this.releasePending(); + + return promise; + } + } + + private async releasePending() { + if(this.releasing) return; + + this.releasing = true; + // this.log('-> messages to send:', this.pending.length); + for(let i = 0; i < this.pending.length; ++i) { + const pending = this.pending[i]; + const {body, resolve} = pending; + + try { + const result = await this._send(body); + resolve(result); + this.pending.splice(i, 1); + } catch(err) { + this.log.error('Send plain request error:', err); + await pause(5000); + } + + --i; + } + + this.releasing = false; + } } diff --git a/src/lib/mtproto/transports/tcpObfuscated.ts b/src/lib/mtproto/transports/tcpObfuscated.ts index 9354b323..6509c13d 100644 --- a/src/lib/mtproto/transports/tcpObfuscated.ts +++ b/src/lib/mtproto/transports/tcpObfuscated.ts @@ -12,6 +12,10 @@ import MTTransport, { MTConnection, MTConnectionConstructable } from "./transpor import intermediatePacketCodec from './intermediate'; import { ConnectionStatus } from "../connectionStatus"; +/// #if MTPROTO_AUTO +import transportController from "./controller"; +/// #endif + export default class TcpObfuscated implements MTTransport { private codec = intermediatePacketCodec; private obfuscation = new Obfuscation(); @@ -29,7 +33,7 @@ export default class TcpObfuscated implements MTTransport { private log: ReturnType; public connected = false; private lastCloseTime: number; - private connection: MTConnection; + public connection: MTConnection; private autoReconnect = true; private reconnectTimeout: number; @@ -53,6 +57,10 @@ export default class TcpObfuscated implements MTTransport { private onOpen = () => { this.connected = true; + /// #if MTPROTO_AUTO + transportController.setTransportOpened('websocket'); + /// #endif + const initPayload = this.obfuscation.init(this.codec); this.connection.send(initPayload); @@ -136,6 +144,12 @@ export default class TcpObfuscated implements MTTransport { }; public clear() { + /// #if MTPROTO_AUTO + if(this.connected) { + transportController.setTransportClosed('websocket'); + } + /// #endif + this.connected = false; if(this.connection) { @@ -183,6 +197,13 @@ export default class TcpObfuscated implements MTTransport { public destroy() { this.setAutoReconnect(false); this.close(); + + this.pending.forEach(pending => { + if(pending.reject) { + pending.reject(); + } + }); + this.pending.length = 0; } public close() { @@ -274,10 +295,6 @@ export default class TcpObfuscated implements MTTransport { 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; let encoded = pending.encoded; @@ -286,25 +303,12 @@ export default class TcpObfuscated implements MTTransport { //this.debugPayloads.push({before: body.slice(), after: enc}); this.debug && this.log.debug('-> body length to send:', body.length); - /* if(this.ws.bufferedAmount) { - this.log.error('bufferedAmount:', this.ws.bufferedAmount); - } */ - - /* if(this.ws.readyState !== this.ws.OPEN) { - this.log.error('ws is closed?'); - this.connected = false; - break; - } */ if(!encoded) { encoded = pending.encoded = this.encodeBody(body); } - //this.lol.push(body); - //setTimeout(() => { - this.connection.send(encoded); - //}, 100); - //this.dd(); + this.connection.send(encoded); if(!pending.resolve) { // remove if no response needed this.pending.splice(i--, 1); diff --git a/src/lib/mtproto/transports/transport.ts b/src/lib/mtproto/transports/transport.ts index 16a18087..faf2bf13 100644 --- a/src/lib/mtproto/transports/transport.ts +++ b/src/lib/mtproto/transports/transport.ts @@ -5,9 +5,13 @@ */ import type EventListenerBase from "../../../helpers/eventListenerBase"; +import type MTPNetworker from "../networker"; export default interface MTTransport { + networker: MTPNetworker; send: (data: Uint8Array) => void; + connected: boolean; + destroy: () => void; } export interface MTConnection extends EventListenerBase<{ @@ -15,7 +19,7 @@ export interface MTConnection extends EventListenerBase<{ message: (buffer: ArrayBuffer) => any, close: () => void, }> { - send: (data: Uint8Array) => void; + send: MTTransport['send']; close: () => void; } diff --git a/src/lib/mtproto/transports/websocket.ts b/src/lib/mtproto/transports/websocket.ts index c0880570..c1236f18 100644 --- a/src/lib/mtproto/transports/websocket.ts +++ b/src/lib/mtproto/transports/websocket.ts @@ -9,6 +9,9 @@ import Modes from '../../../config/modes'; import EventListenerBase from '../../../helpers/eventListenerBase'; import { MTConnection } from './transport'; +// let closeSocketBefore = Date.now() + 30e3; +// let closeSocketAfter = Date.now() + 10e3; + export default class Socket extends EventListenerBase<{ open: () => void, message: (buffer: ArrayBuffer) => any, @@ -49,6 +52,11 @@ export default class Socket extends EventListenerBase<{ this.ws.addEventListener('close', this.handleClose); this.ws.addEventListener('error', this.handleError); this.ws.addEventListener('message', this.handleMessage); + + // if(Date.now() < closeSocketBefore) { + // if(Date.now() >= closeSocketAfter) { + // this.ws.close(); + // } } public close() { diff --git a/src/pages/pageIm.ts b/src/pages/pageIm.ts index b5e59b7c..acc2f6c8 100644 --- a/src/pages/pageIm.ts +++ b/src/pages/pageIm.ts @@ -35,6 +35,7 @@ let onFirstMount = () => { // setTimeout(() => { const promise = import('../lib/appManagers/appDialogsManager'); promise.finally(async() => { + document.getElementById('auth-pages').remove(); //alert('pageIm!'); resolve(); diff --git a/src/pages/pagePassword.ts b/src/pages/pagePassword.ts index b2e34f14..705bbbd8 100644 --- a/src/pages/pagePassword.ts +++ b/src/pages/pagePassword.ts @@ -84,6 +84,9 @@ let onFirstMount = (): Promise => { btnNextI18n.update({key: 'PleaseWait'}); const preloader = putPreloader(btnNext); + passwordInputField.setValueSilently('' + Math.random()); // prevent saving suggestion + passwordInputField.setValueSilently(value); // prevent saving suggestion + passwordManager.check(value, state).then((response) => { //console.log('passwordManager response:', response); diff --git a/src/scss/partials/_input.scss b/src/scss/partials/_input.scss index 5d768de4..ddfab5ed 100644 --- a/src/scss/partials/_input.scss +++ b/src/scss/partials/_input.scss @@ -393,6 +393,9 @@ input:focus, button:focus { &[type="password"] { font-size: 2.25rem; padding-left: calc(.875rem - var(--border-width)); + line-height: 1; + padding-top: 0; + padding-bottom: 0; @media (-webkit-min-device-pixel-ratio: 2) { font-size: 1.75rem; diff --git a/src/scss/style.scss b/src/scss/style.scss index adb401ac..b5993bcb 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -555,6 +555,14 @@ input, textarea { -webkit-appearance: none; } +// Possible fix Safari's password autocomplete +input::-webkit-contacts-auto-fill-button, +input::-webkit-credentials-auto-fill-button { + visibility: hidden; + position: absolute; + right: 0; +} + /* input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, diff --git a/webpack.common.js b/webpack.common.js index 2b94baf9..4aae7ee6 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -17,11 +17,17 @@ if(devMode) { console.log('DEVMODE IS ON!'); } +const MTPROTO_HTTP = false; +const MTPROTO_AUTO = true; + const opts = { MTPROTO_WORKER: true, MTPROTO_SW: false, - MTPROTO_HTTP: false, + MTPROTO_HTTP: MTPROTO_HTTP, MTPROTO_HTTP_UPLOAD: false, + MTPROTO_AUTO: MTPROTO_AUTO, // use HTTPS when WS is unavailable + MTPROTO_HAS_HTTP: MTPROTO_AUTO, + MTPROTO_HAS_WS: MTPROTO_AUTO || !MTPROTO_HTTP, DEBUG: devMode, version: 3, 'ifdef-verbose': devMode, // add this for verbose output