Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
285 lines
7.9 KiB
285 lines
7.9 KiB
/* |
|
* 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<any, Promise<any>> |
|
} = {}; |
|
private apiPromisesSingle: { |
|
[q: string]: Promise<any> |
|
} = {}; |
|
private apiPromisesCacheable: { |
|
[method: string]: { |
|
[queryJSON: string]: { |
|
timestamp: number, |
|
promise: Promise<any>, |
|
fulfilled: boolean, |
|
timeout?: number, |
|
params: any |
|
} |
|
} |
|
} = {}; |
|
|
|
private config: Config; |
|
private 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} |
|
}) { |
|
// @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<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] = {}; |
|
this.hashes[method][queryJSON] = { |
|
hash, |
|
result |
|
}; |
|
} |
|
|
|
if(o.processResult) { |
|
return o.processResult(result); |
|
} |
|
|
|
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] ?? (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>(method: T, params: MethodDeclMap[T]['req'] = {} as any, options: InvokeApiOptions & Partial<{cacheSeconds: number, override: boolean}> = {}): Promise<MethodDeclMap[T]['res']> { |
|
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<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: {}, |
|
processResult: (config: MTAppConfig) => { |
|
this.appConfig = config; |
|
ignoreRestrictionReasons(config.ignore_restriction_reasons ?? []); |
|
this.rootScope.dispatchEvent('app_config', config); |
|
return config; |
|
}, |
|
options: {overwrite} |
|
}); |
|
} |
|
}
|
|
|