431 lines
14 KiB
TypeScript
431 lines
14 KiB
TypeScript
import sessionStorage from '../sessionStorage';
|
||
|
||
import MTPNetworker, { MTMessage } from './networker';
|
||
import { isObject } from './bin_utils';
|
||
import networkerFactory from './networkerFactory';
|
||
//import { telegramMeWebService } from './mtproto';
|
||
import authorizer from './authorizer';
|
||
import dcConfigurator, { ConnectionType, TransportType } from './dcConfigurator';
|
||
import { logger } from '../logger';
|
||
import type { InvokeApiOptions } from '../../types';
|
||
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';
|
||
import App from '../../config/app';
|
||
import { MOUNT_CLASS_TO } from '../../config/debug';
|
||
|
||
/// #if !MTPROTO_WORKER
|
||
import rootScope from '../rootScope';
|
||
/// #endif
|
||
|
||
/* var networker = apiManager.cachedNetworkers.websocket.upload[2];
|
||
networker.wrapMtpMessage({
|
||
_: 'msgs_state_req',
|
||
msg_ids: ["6888292542796810828"]
|
||
}, {
|
||
notContentRelated: true
|
||
}).then(res => {
|
||
console.log('status', res);
|
||
}); */
|
||
|
||
//console.error('apiManager included!');
|
||
// TODO: если запрос словил флуд, нужно сохранять его параметры и возвращать тот же промис на новый такой же запрос, например - загрузка истории
|
||
|
||
export type ApiError = Partial<{
|
||
code: number,
|
||
type: string,
|
||
description: string,
|
||
originalError: any,
|
||
stack: string,
|
||
handled: boolean,
|
||
input: string,
|
||
message: ApiError
|
||
}>;
|
||
|
||
/* class RotatableArray<T> {
|
||
public array: Array<T> = [];
|
||
private lastIndex = -1;
|
||
|
||
public get() {
|
||
this.lastIndex = clamp(this.lastIndex + 1, 0, this.array.length - 1);
|
||
return this.array[this.lastIndex];
|
||
}
|
||
} */
|
||
|
||
export class ApiManager {
|
||
public cachedNetworkers: {
|
||
[transportType in TransportType]: {
|
||
[connectionType in ConnectionType]: {
|
||
[dcId: number]: MTPNetworker[]
|
||
}
|
||
}
|
||
} = {} as any;
|
||
|
||
public cachedExportPromise: {[x: number]: Promise<unknown>} = {};
|
||
private gettingNetworkers: {[dcIdAndType: string]: Promise<MTPNetworker>} = {};
|
||
public baseDcId = 0;
|
||
|
||
//public telegramMeNotified = false;
|
||
|
||
private log: ReturnType<typeof logger> = logger('API');
|
||
|
||
private afterMessageTempIds: {[tempId: string]: string} = {};
|
||
|
||
//private lol = false;
|
||
|
||
constructor() {
|
||
//MtpSingleInstanceService.start();
|
||
|
||
/* AppStorage.get<number>('dc').then((dcId) => {
|
||
if(dcId) {
|
||
this.baseDcId = dcId;
|
||
}
|
||
}); */
|
||
}
|
||
|
||
/* public telegramMeNotify(newValue: boolean) {
|
||
if(this.telegramMeNotified !== newValue) {
|
||
this.telegramMeNotified = newValue;
|
||
//telegramMeWebService.setAuthorized(this.telegramMeNotified);
|
||
}
|
||
} */
|
||
|
||
// mtpSetUserAuth
|
||
public setUserAuth(userId: number) {
|
||
sessionStorage.set({
|
||
user_auth: userId
|
||
});
|
||
|
||
//this.telegramMeNotify(true);
|
||
|
||
/// #if !MTPROTO_WORKER
|
||
rootScope.broadcast('user_auth', userId);
|
||
/// #endif
|
||
}
|
||
|
||
public setBaseDcId(dcId: number) {
|
||
this.baseDcId = dcId;
|
||
|
||
sessionStorage.set({
|
||
dc: this.baseDcId
|
||
});
|
||
}
|
||
|
||
// mtpLogOut
|
||
public async logOut() {
|
||
const storageKeys: Array<string> = [];
|
||
|
||
const prefix = 'dc';
|
||
for(let dcId = 1; dcId <= 5; dcId++) {
|
||
storageKeys.push(prefix + dcId + '_auth_key');
|
||
//storageKeys.push(prefix + dcId + '_auth_keyId');
|
||
}
|
||
|
||
// WebPushApiManager.forceUnsubscribe(); // WARNING
|
||
const storageResult = await Promise.all(storageKeys.map(key => sessionStorage.get(key as any)));
|
||
|
||
const logoutPromises = [];
|
||
for(let i = 0; i < storageResult.length; i++) {
|
||
if(storageResult[i]) {
|
||
logoutPromises.push(this.invokeApi('auth.logOut', {}, {dcId: i + 1, ignoreErrors: true}));
|
||
}
|
||
}
|
||
|
||
const clear = () => {
|
||
//console.error('apiManager: logOut clear');
|
||
|
||
this.baseDcId = 0;
|
||
//this.telegramMeNotify(false);
|
||
const promise = sessionStorage.clear();
|
||
promise.finally(() => {
|
||
self.postMessage({type: 'reload'});
|
||
});
|
||
};
|
||
|
||
setTimeout(clear, 1e3);
|
||
|
||
//return;
|
||
|
||
return Promise.all(logoutPromises).then(() => {
|
||
}, (error) => {
|
||
error.handled = true;
|
||
}).finally(clear)/* .then(() => {
|
||
location.pathname = '/';
|
||
}) */;
|
||
}
|
||
|
||
// mtpGetNetworker
|
||
public getNetworker(dcId: number, options: InvokeApiOptions = {}): Promise<MTPNetworker> {
|
||
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' && isSafari ? 'https' : 'websocket';
|
||
//const transportType: TransportType = connectionType !== 'client' ? 'https' : 'websocket';
|
||
/// #else
|
||
// @ts-ignore
|
||
const transportType = 'websocket';
|
||
/// #endif
|
||
|
||
if(!this.cachedNetworkers.hasOwnProperty(transportType)) {
|
||
this.cachedNetworkers[transportType] = {
|
||
client: {},
|
||
download: {},
|
||
upload: {}
|
||
};
|
||
}
|
||
|
||
const cache = this.cachedNetworkers[transportType][connectionType];
|
||
if(!(dcId in cache)) {
|
||
cache[dcId] = [];
|
||
}
|
||
|
||
const networkers = cache[dcId];
|
||
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) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
const networker = found ? networkers.splice(i, 1)[0] : networkers.pop();
|
||
networkers.unshift(networker);
|
||
return Promise.resolve(networker);
|
||
}
|
||
|
||
const getKey = [dcId, transportType, connectionType].join('-');
|
||
if(this.gettingNetworkers[getKey]) {
|
||
return this.gettingNetworkers[getKey];
|
||
}
|
||
|
||
const ak = 'dc' + dcId + '_auth_key';
|
||
const akId = 'dc' + dcId + '_auth_keyId';
|
||
const ss = 'dc' + dcId + '_server_salt';
|
||
|
||
return this.gettingNetworkers[getKey] = Promise.all([ak, akId, ss].map(key => sessionStorage.get(key as any)))
|
||
.then(async([authKeyHex, authKeyIdHex, serverSaltHex]) => {
|
||
const transport = dcConfigurator.chooseServer(dcId, connectionType, transportType, false);
|
||
let networker: MTPNetworker;
|
||
if(authKeyHex && authKeyHex.length === 512) {
|
||
if(!serverSaltHex || serverSaltHex.length !== 16) {
|
||
serverSaltHex = 'AAAAAAAAAAAAAAAA';
|
||
}
|
||
|
||
const authKey = bytesFromHex(authKeyHex);
|
||
const authKeyId = new Uint8Array(bytesFromHex(authKeyIdHex));
|
||
const serverSalt = bytesFromHex(serverSaltHex);
|
||
|
||
networker = networkerFactory.getNetworker(dcId, authKey, authKeyId, serverSalt, transport, options);
|
||
} else {
|
||
try { // if no saved state
|
||
const auth = await authorizer.auth(dcId);
|
||
|
||
const storeObj = {
|
||
[ak]: bytesToHex(auth.authKey),
|
||
[akId]: auth.authKeyId.hex,
|
||
[ss]: bytesToHex(auth.serverSalt)
|
||
};
|
||
|
||
sessionStorage.set(storeObj);
|
||
|
||
networker = networkerFactory.getNetworker(dcId, auth.authKey, auth.authKeyId, auth.serverSalt, transport, options);
|
||
} catch(error) {
|
||
this.log('Get networker error', error, error.stack);
|
||
delete this.gettingNetworkers[getKey];
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/* networker.onConnectionStatusChange = (online) => {
|
||
console.log('status:', online);
|
||
}; */
|
||
|
||
delete this.gettingNetworkers[getKey];
|
||
networkers.unshift(networker);
|
||
return networker;
|
||
});
|
||
}
|
||
|
||
// mtpInvokeApi
|
||
public invokeApi<T extends keyof MethodDeclMap>(method: T, params: MethodDeclMap[T]['req'] = {}, options: InvokeApiOptions = {}): CancellablePromise<MethodDeclMap[T]["res"]> {
|
||
///////this.log('Invoke api', method, params, options);
|
||
|
||
/* if(!this.lol) {
|
||
networkerFactory.updatesProcessor({_: 'new_session_created'}, true);
|
||
this.lol = true;
|
||
} */
|
||
|
||
const deferred = deferredPromise<MethodDeclMap[T]['res']>();
|
||
|
||
let afterMessageIdTemp = options.afterMessageId;
|
||
if(afterMessageIdTemp) {
|
||
deferred.finally(() => {
|
||
delete this.afterMessageTempIds[afterMessageIdTemp];
|
||
});
|
||
}
|
||
|
||
if(MOUNT_CLASS_TO) {
|
||
deferred.finally(() => {
|
||
clearInterval(interval);
|
||
});
|
||
|
||
const startTime = Date.now();
|
||
const interval = MOUNT_CLASS_TO.setInterval(() => {
|
||
this.log.error('Request is still processing:', method, params, options, 'time:', (Date.now() - startTime) / 1000);
|
||
//this.cachedUploadNetworkers[2].requestMessageStatus();
|
||
}, 5e3);
|
||
}
|
||
|
||
const rejectPromise = (error: ApiError) => {
|
||
if(!error) {
|
||
error = {type: 'ERROR_EMPTY'};
|
||
} else if(!isObject(error)) {
|
||
error = {message: error};
|
||
}
|
||
|
||
deferred.reject(error);
|
||
|
||
if(error.code === 401 && error.type === 'SESSION_REVOKED') {
|
||
this.logOut();
|
||
}
|
||
|
||
if(options.ignoreErrors) {
|
||
return;
|
||
}
|
||
|
||
if(error.code === 406) {
|
||
error.handled = true;
|
||
}
|
||
|
||
if(!options.noErrorBox) {
|
||
error.input = method;
|
||
error.stack = stack || (error.originalError && error.originalError.stack) || error.stack || (new Error()).stack;
|
||
setTimeout(() => {
|
||
if(!error.handled) {
|
||
if(error.code === 401) {
|
||
this.logOut();
|
||
} else {
|
||
// ErrorService.show({error: error}); // WARNING
|
||
}
|
||
|
||
error.handled = true;
|
||
}
|
||
}, 100);
|
||
}
|
||
};
|
||
|
||
let dcId: number;
|
||
|
||
let cachedNetworker: MTPNetworker;
|
||
let stack = (new Error()).stack || 'empty stack';
|
||
const performRequest = (networker: MTPNetworker) => {
|
||
if(afterMessageIdTemp) {
|
||
options.afterMessageId = this.afterMessageTempIds[afterMessageIdTemp];
|
||
}
|
||
const promise = (cachedNetworker = networker).wrapApiCall(method, params, options);
|
||
if(options.prepareTempMessageId) {
|
||
this.afterMessageTempIds[options.prepareTempMessageId] = (options as MTMessage).messageId;
|
||
}
|
||
|
||
return promise.then(deferred.resolve, (error: ApiError) => {
|
||
//if(!options.ignoreErrors) {
|
||
if(error.type !== 'FILE_REFERENCE_EXPIRED' && error.type !== 'MSG_WAIT_FAILED') {
|
||
this.log.error('Error', error.code, error.type, this.baseDcId, dcId, method, params);
|
||
}
|
||
|
||
if(error.code === 401 && this.baseDcId === dcId) {
|
||
if(error.type !== 'SESSION_PASSWORD_NEEDED') {
|
||
sessionStorage.remove('dc')
|
||
sessionStorage.remove('user_auth'); // ! возможно тут вообще не нужно это делать, но нужно проверить случай с USER_DEACTIVATED (https://core.telegram.org/api/errors)
|
||
//this.telegramMeNotify(false);
|
||
}
|
||
|
||
rejectPromise(error);
|
||
} else if(error.code === 401 && this.baseDcId && dcId !== this.baseDcId) {
|
||
if(this.cachedExportPromise[dcId] === undefined) {
|
||
const promise = new Promise((exportResolve, exportReject) => {
|
||
this.invokeApi('auth.exportAuthorization', {dc_id: dcId}, {noErrorBox: true}).then((exportedAuth) => {
|
||
this.invokeApi('auth.importAuthorization', {
|
||
id: exportedAuth.id,
|
||
bytes: exportedAuth.bytes
|
||
}, {dcId, noErrorBox: true}).then(exportResolve, exportReject);
|
||
}, exportReject);
|
||
});
|
||
|
||
this.cachedExportPromise[dcId] = promise;
|
||
}
|
||
|
||
this.cachedExportPromise[dcId].then(() => {
|
||
//(cachedNetworker = networker).wrapApiCall(method, params, options).then(deferred.resolve, rejectPromise);
|
||
this.invokeApi(method, params, options).then(deferred.resolve, rejectPromise);
|
||
}, rejectPromise);
|
||
} else if(error.code === 303) {
|
||
const newDcId = +error.type.match(/^(PHONE_MIGRATE_|NETWORK_MIGRATE_|USER_MIGRATE_|FILE_MIGRATE_)(\d+)/)[2];
|
||
if(newDcId !== dcId) {
|
||
if(options.dcId) {
|
||
options.dcId = newDcId;
|
||
} else {
|
||
this.setBaseDcId(newDcId);
|
||
}
|
||
|
||
this.getNetworker(newDcId, options).then((networker) => {
|
||
networker.wrapApiCall(method, params, options).then(deferred.resolve, rejectPromise);
|
||
}, rejectPromise);
|
||
}
|
||
} else if(!options.rawError && error.code === 420) {
|
||
const waitTime = +error.type.match(/^FLOOD_WAIT_(\d+)/)[1] || 10;
|
||
|
||
if(waitTime > (options.floodMaxTimeout !== undefined ? options.floodMaxTimeout : 60)) {
|
||
return rejectPromise(error);
|
||
}
|
||
|
||
setTimeout(() => {
|
||
performRequest(cachedNetworker);
|
||
}, waitTime/* (waitTime + 5) */ * 1000); // 03.02.2020
|
||
} else if(!options.rawError && error.code === 500) {
|
||
if(error.type === 'MSG_WAIT_FAILED') {
|
||
afterMessageIdTemp = undefined;
|
||
delete options.afterMessageId;
|
||
delete this.afterMessageTempIds[options.prepareTempMessageId];
|
||
performRequest(cachedNetworker);
|
||
return;
|
||
}
|
||
|
||
const now = Date.now();
|
||
if(options.stopTime) {
|
||
if(now >= options.stopTime) {
|
||
return rejectPromise(error);
|
||
}
|
||
}
|
||
|
||
options.waitTime = options.waitTime ? Math.min(60, options.waitTime * 1.5) : 1;
|
||
setTimeout(() => {
|
||
performRequest(cachedNetworker);
|
||
}, options.waitTime * 1000);
|
||
} else {
|
||
rejectPromise(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
if(dcId = (options.dcId || this.baseDcId)) {
|
||
this.getNetworker(dcId, options).then(performRequest, rejectPromise);
|
||
} else {
|
||
sessionStorage.get('dc').then((baseDcId) => {
|
||
this.getNetworker(this.baseDcId = dcId = baseDcId || App.baseDcId, options).then(performRequest, rejectPromise);
|
||
});
|
||
}
|
||
|
||
return deferred;
|
||
}
|
||
}
|
||
|
||
const apiManager = new ApiManager();
|
||
MOUNT_CLASS_TO && (MOUNT_CLASS_TO.apiManager = apiManager);
|
||
export default apiManager;
|