352 lines
10 KiB
TypeScript
352 lines
10 KiB
TypeScript
/*
|
|
* https://github.com/morethanwords/tweb
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
*/
|
|
|
|
import ctx from '../../environment/ctx';
|
|
import assumeType from '../../helpers/assumeType';
|
|
import callbackify from '../../helpers/callbackify';
|
|
import {ignoreRestrictionReasons} from '../../helpers/restrictions';
|
|
import {Config, HelpAppConfig, MethodDeclMap, User} from '../../layer';
|
|
import {InvokeApiOptions} from '../../types';
|
|
import {AppManager} from '../appManagers/manager';
|
|
import {MTAppConfig} from './appConfig';
|
|
import {UserAuth} from './mtproto_config';
|
|
import {MTMessage} from './networker';
|
|
|
|
type HashResult = {
|
|
hash: number,
|
|
result: any
|
|
};
|
|
|
|
type HashOptions = {
|
|
[queryJSON: string]: HashResult
|
|
};
|
|
|
|
export type ApiLimitType = 'pin' | 'folderPin' | 'folders' |
|
|
'favedStickers' | 'reactions' | 'bio' | 'topicPin' | 'caption';
|
|
|
|
export default abstract class ApiManagerMethods extends AppManager {
|
|
private afterMessageIdTemp: number;
|
|
private hashes: {[method: string]: HashOptions} = {};
|
|
private apiPromisesSingleProcess: {
|
|
[q: string]: Map<any, Promise<any>>
|
|
} = {};
|
|
private apiPromisesSingle: {
|
|
[q: string]: Promise<any>
|
|
} = {};
|
|
private apiPromisesCacheable: {
|
|
[method: string]: {
|
|
[queryJSON: string]: {
|
|
timestamp: number,
|
|
promise: Promise<any>,
|
|
fulfilled: boolean,
|
|
result?: any,
|
|
error?: any,
|
|
timeout?: number,
|
|
params: any
|
|
}
|
|
}
|
|
} = {};
|
|
|
|
protected config: Config;
|
|
protected appConfig: MTAppConfig;
|
|
|
|
constructor() {
|
|
super();
|
|
this.afterMessageIdTemp = 0;
|
|
}
|
|
|
|
abstract setUserAuth(userAuth: UserAuth | UserId): Promise<void>;
|
|
|
|
public setUser(user: User) {
|
|
// appUsersManager.saveApiUser(user);
|
|
return this.setUserAuth(user.id);
|
|
}
|
|
|
|
abstract invokeApi<T extends keyof MethodDeclMap>(method: T, params?: MethodDeclMap[T]['req'], options?: InvokeApiOptions): Promise<MethodDeclMap[T]['res']>;
|
|
|
|
public invokeApiAfter<T extends keyof MethodDeclMap>(method: T, params: MethodDeclMap[T]['req'] = {}, options: InvokeApiOptions = {}): Promise<MethodDeclMap[T]['res']> {
|
|
let o = options;
|
|
o.prepareTempMessageId = '' + ++this.afterMessageIdTemp;
|
|
|
|
o = {...options};
|
|
(options as MTMessage).messageId = o.prepareTempMessageId;
|
|
|
|
// console.log('will invokeApi:', method, params, options);
|
|
return this.invokeApi(method, params, o);
|
|
}
|
|
|
|
public invokeApiHashable<T extends keyof MethodDeclMap, R>(o: {
|
|
method: T,
|
|
processResult?: (response: MethodDeclMap[T]['res']) => R,
|
|
processError?: (error: ApiError) => any,
|
|
params?: Omit<MethodDeclMap[T]['req'], 'hash'>,
|
|
options?: InvokeApiOptions & {cacheKey?: string},
|
|
overwrite?: boolean
|
|
}) {
|
|
// @ts-ignore
|
|
o.params ??= {};
|
|
o.options ??= {};
|
|
// console.log('will invokeApi:', method, params, options);
|
|
|
|
const {params, options, method, overwrite} = o;
|
|
|
|
const queryJSON = JSON.stringify(params);
|
|
let cached: HashResult;
|
|
if(this.hashes[method]) {
|
|
cached = this.hashes[method][queryJSON];
|
|
if(cached) {
|
|
if(overwrite) {
|
|
delete this.hashes[method][queryJSON];
|
|
cached = undefined;
|
|
} else {
|
|
(params as any).hash = cached.hash;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this.invokeApiSingleProcess<T, R>({
|
|
method,
|
|
processResult: (result) => {
|
|
if(result._.includes('NotModified')) {
|
|
// this.debug && this.log.warn('NotModified saved!', method, queryJSON);
|
|
return cached.result;
|
|
}
|
|
|
|
if(result.hash/* || result.messages */) {
|
|
const hash = result.hash/* || this.computeHash(result.messages) */;
|
|
|
|
if(!this.hashes[method]) this.hashes[method] = {};
|
|
cached = this.hashes[method][queryJSON] = {
|
|
hash,
|
|
result
|
|
};
|
|
}
|
|
|
|
if(o.processResult) {
|
|
const newResult = o.processResult(result);
|
|
return cached ? cached.result = newResult : newResult;
|
|
}
|
|
|
|
return result;
|
|
},
|
|
params,
|
|
options
|
|
});
|
|
}
|
|
|
|
public invokeApiSingle<T extends keyof MethodDeclMap>(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions = {}): Promise<MethodDeclMap[T]['res']> {
|
|
const q = method + '-' + JSON.stringify(params);
|
|
const cache = this.apiPromisesSingle;
|
|
if(cache[q]) {
|
|
return cache[q];
|
|
}
|
|
|
|
return cache[q] = this.invokeApi(method, params, options).finally(() => {
|
|
delete cache[q];
|
|
});
|
|
}
|
|
|
|
public invokeApiSingleProcess<T extends keyof MethodDeclMap, R>(o: {
|
|
method: T,
|
|
processResult: (response: MethodDeclMap[T]['res']) => R,
|
|
processError?: (error: ApiError) => any,
|
|
params?: MethodDeclMap[T]['req'],
|
|
options?: InvokeApiOptions & {cacheKey?: string, overwrite?: boolean}
|
|
}): Promise<Awaited<R>> {
|
|
o.params ??= {};
|
|
o.options ??= {};
|
|
|
|
const {method, processResult, processError, params, options} = o;
|
|
const cache = this.apiPromisesSingleProcess;
|
|
const cacheKey = options.cacheKey || JSON.stringify(params);
|
|
const map = cache[method] ??= new Map();
|
|
const oldPromise = map.get(cacheKey);
|
|
if(oldPromise) {
|
|
return oldPromise;
|
|
}
|
|
|
|
const getNewPromise = () => {
|
|
const promise = map.get(cacheKey);
|
|
return promise === p ? undefined : promise;
|
|
}
|
|
|
|
const originalPromise = this.invokeApi(method, params, options);
|
|
const newPromise: Promise<Awaited<R>> = originalPromise.then((result) => {
|
|
return getNewPromise() || processResult(result);
|
|
}, (error) => {
|
|
const promise = getNewPromise();
|
|
if(promise) {
|
|
return promise;
|
|
}
|
|
|
|
if(!processError) {
|
|
throw error;
|
|
}
|
|
|
|
return processError(error);
|
|
});
|
|
|
|
const p = newPromise.finally(() => {
|
|
if(map.get(cacheKey) !== p) {
|
|
return;
|
|
}
|
|
|
|
map.delete(cacheKey);
|
|
if(!map.size) {
|
|
delete cache[method];
|
|
}
|
|
});
|
|
|
|
map.set(cacheKey, p);
|
|
return p;
|
|
}
|
|
|
|
public invokeApiCacheable<
|
|
T extends keyof MethodDeclMap,
|
|
O extends InvokeApiOptions & Partial<{cacheSeconds: number, override: boolean, syncIfHasResult: boolean}>
|
|
>(
|
|
method: T,
|
|
params: MethodDeclMap[T]['req'] = {} as any,
|
|
options: O = {} as any
|
|
): O['syncIfHasResult'] extends true ? MethodDeclMap[T]['res'] | Promise<MethodDeclMap[T]['res']> : Promise<MethodDeclMap[T]['res']> {
|
|
const cache = this.apiPromisesCacheable[method] ??= {};
|
|
const queryJSON = JSON.stringify(params);
|
|
let item = cache[queryJSON];
|
|
if(item && (!options.override || !item.fulfilled)) {
|
|
if(options.syncIfHasResult) {
|
|
if(item.hasOwnProperty('result')) {
|
|
return item.result;
|
|
} else if(item.hasOwnProperty('error')) {
|
|
throw item.error;
|
|
}
|
|
}
|
|
|
|
return item.promise;
|
|
}
|
|
|
|
if(options.override) {
|
|
if(item?.timeout) {
|
|
clearTimeout(item.timeout);
|
|
delete item.timeout;
|
|
}
|
|
|
|
delete options.override;
|
|
}
|
|
|
|
let timeout: number;
|
|
if(options.cacheSeconds) {
|
|
timeout = ctx.setTimeout(() => {
|
|
if(cache[queryJSON] === item) {
|
|
delete cache[queryJSON];
|
|
}
|
|
}, options.cacheSeconds * 1000);
|
|
delete options.cacheSeconds;
|
|
}
|
|
|
|
const promise = this.invokeApi(method, params, options);
|
|
|
|
const onResult = (result: any) => {
|
|
item.result = result;
|
|
};
|
|
|
|
promise.then((result) => {
|
|
item.result = result;
|
|
}, (error) => {
|
|
item.error = error;
|
|
});
|
|
|
|
item = cache[queryJSON] = {
|
|
timestamp: Date.now(),
|
|
fulfilled: false,
|
|
timeout,
|
|
promise,
|
|
params
|
|
};
|
|
|
|
return promise;
|
|
}
|
|
|
|
public clearCache<T extends keyof MethodDeclMap>(method: T, verify: (params: MethodDeclMap[T]['req']) => boolean) {
|
|
const cache = this.apiPromisesCacheable[method];
|
|
if(cache) {
|
|
for(const queryJSON in cache) {
|
|
const item = cache[queryJSON];
|
|
try {
|
|
if(verify(item.params)) {
|
|
if(item.timeout) {
|
|
clearTimeout(item.timeout);
|
|
}
|
|
|
|
delete cache[queryJSON];
|
|
}
|
|
} catch(err) {
|
|
// this.log.error('clearCache error:', err, queryJSON, item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public getConfig(overwrite?: boolean) {
|
|
if(this.config && !overwrite) {
|
|
return this.config;
|
|
}
|
|
|
|
return this.invokeApiSingleProcess({
|
|
method: 'help.getConfig',
|
|
params: {},
|
|
processResult: (config) => {
|
|
this.config = config;
|
|
this.rootScope.dispatchEvent('config', config);
|
|
return config;
|
|
},
|
|
options: {overwrite}
|
|
});
|
|
}
|
|
|
|
public getAppConfig(overwrite?: boolean) {
|
|
if(this.appConfig && !overwrite) {
|
|
return this.appConfig;
|
|
}
|
|
|
|
return this.invokeApiSingleProcess({
|
|
method: 'help.getAppConfig',
|
|
params: {
|
|
hash: 0
|
|
},
|
|
processResult: (helpAppConfig) => {
|
|
assumeType<HelpAppConfig.helpAppConfig>(helpAppConfig);
|
|
const config = helpAppConfig.config as MTAppConfig;
|
|
this.appConfig = config;
|
|
ignoreRestrictionReasons(config.ignore_restriction_reasons ?? []);
|
|
this.rootScope.dispatchEvent('app_config', config);
|
|
return config;
|
|
},
|
|
options: {overwrite}
|
|
});
|
|
}
|
|
|
|
public getLimit(type: ApiLimitType, isPremium?: boolean) {
|
|
return callbackify(this.getAppConfig(), (appConfig) => {
|
|
const map: {[type in ApiLimitType]: [keyof MTAppConfig, keyof MTAppConfig] | keyof MTAppConfig} = {
|
|
pin: ['dialogs_pinned_limit_default', 'dialogs_pinned_limit_premium'],
|
|
folderPin: ['dialogs_folder_pinned_limit_default', 'dialogs_folder_pinned_limit_premium'],
|
|
folders: ['dialog_filters_limit_default', 'dialog_filters_limit_premium'],
|
|
favedStickers: ['stickers_faved_limit_default', 'stickers_faved_limit_premium'],
|
|
reactions: ['reactions_user_max_default', 'reactions_user_max_premium'],
|
|
bio: ['about_length_limit_default', 'about_length_limit_premium'],
|
|
topicPin: 'topics_pinned_limit',
|
|
caption: ['caption_length_limit_default', 'caption_length_limit_premium']
|
|
};
|
|
|
|
isPremium ??= this.rootScope.premium;
|
|
|
|
const a = map[type];
|
|
const key = Array.isArray(a) ? a[isPremium ? 1 : 0] : a;
|
|
return appConfig[key] as number;
|
|
});
|
|
}
|
|
}
|