/* * 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 {ignoreRestrictionReasons} from '../../helpers/restrictions'; import {Config, 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 default abstract class ApiManagerMethods extends AppManager { private afterMessageIdTemp: number; private hashes: {[method: string]: HashOptions} = {}; private apiPromisesSingleProcess: { [q: string]: Map> } = {}; private apiPromisesSingle: { [q: string]: Promise } = {}; private apiPromisesCacheable: { [method: string]: { [queryJSON: string]: { timestamp: number, promise: Promise, fulfilled: boolean, timeout?: number, params: any } } } = {}; private config: Config; private appConfig: MTAppConfig; constructor() { super(); this.afterMessageIdTemp = 0; } abstract setUserAuth(userAuth: UserAuth | UserId): Promise; public setUser(user: User) { // appUsersManager.saveApiUser(user); return this.setUserAuth(user.id); } abstract invokeApi(method: T, params?: MethodDeclMap[T]['req'], options?: InvokeApiOptions): Promise; public invokeApiAfter(method: T, params: MethodDeclMap[T]['req'] = {}, options: InvokeApiOptions = {}): Promise { 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(o: { method: T, processResult?: (response: MethodDeclMap[T]['res']) => R, processError?: (error: ApiError) => any, params?: Omit, options?: InvokeApiOptions & {cacheKey?: string} }) { // @ts-ignore o.params ??= {}; o.options ??= {}; // console.log('will invokeApi:', method, params, options); const {params, options, method} = o; const queryJSON = JSON.stringify(params); let cached: HashResult; if(this.hashes[method]) { cached = this.hashes[method][queryJSON]; if(cached) { (params as any).hash = cached.hash; } } return this.invokeApiSingleProcess({ 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] = {}; this.hashes[method][queryJSON] = { hash, result }; } if(o.processResult) { return o.processResult(result); } return result; }, params, options }); } public invokeApiSingle(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions = {}): Promise { 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(o: { method: T, processResult: (response: MethodDeclMap[T]['res']) => R, processError?: (error: ApiError) => any, params?: MethodDeclMap[T]['req'], options?: InvokeApiOptions & {cacheKey?: string, overwrite?: boolean} }): Promise> { 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] ?? (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> = 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(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions & Partial<{cacheSeconds: number, override: boolean}> = {}): Promise { const cache = this.apiPromisesCacheable[method] ?? (this.apiPromisesCacheable[method] = {}); const queryJSON = JSON.stringify(params); const item = cache[queryJSON]; if(item && (!options.override || !item.fulfilled)) { return item.promise; } if(options.override) { if(item && item.timeout) { clearTimeout(item.timeout); delete item.timeout; } delete options.override; } let timeout: number; if(options.cacheSeconds) { timeout = ctx.setTimeout(() => { delete cache[queryJSON]; }, options.cacheSeconds * 1000); delete options.cacheSeconds; } const promise = this.invokeApi(method, params, options); cache[queryJSON] = { timestamp: Date.now(), fulfilled: false, timeout, promise, params }; return promise; } public clearCache(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: {}, processResult: (config: MTAppConfig) => { this.appConfig = config; ignoreRestrictionReasons(config.ignore_restriction_reasons ?? []); this.rootScope.dispatchEvent('app_config', config); return config; }, options: {overwrite} }); } }