2021-11-19 03:22:35 +03:00

311 lines
8.6 KiB
TypeScript

import {
TelegramClient, sessions, Api as GramJs, connection,
} from '../../../lib/gramjs';
import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index';
import { TwoFaParams } from '../../../lib/gramjs/client/2fa';
import {
ApiInitialArgs,
ApiMediaFormat,
ApiOnProgress,
ApiSessionData,
OnApiUpdate,
} from '../../types';
import {
DEBUG, DEBUG_GRAMJS, UPLOAD_WORKERS, IS_TEST, APP_VERSION,
} from '../../../config';
import {
onRequestPhoneNumber, onRequestCode, onRequestPassword, onRequestRegistration,
onAuthError, onAuthReady, onCurrentUserUpdate, onRequestQrCode,
} from './auth';
import { updater } from '../updater';
import { setMessageBuilderCurrentUserId } from '../apiBuilders/messages';
import downloadMediaWithClient from './media';
import { buildApiUserFromFull } from '../apiBuilders/users';
import localDb from '../localDb';
import { buildApiPeerId } from '../apiBuilders/peers';
const DEFAULT_USER_AGENT = 'Unknown UserAgent';
const DEFAULT_PLATFORM = 'Unknown platform';
const APP_CODE_NAME = 'Z';
GramJsLogger.setLevel(DEBUG_GRAMJS ? 'debug' : 'warn');
const gramJsUpdateEventBuilder = { build: (update: object) => update };
let onUpdate: OnApiUpdate;
let client: TelegramClient;
let isConnected = false;
export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('>>> START INIT API');
}
onUpdate = _onUpdate;
const {
userAgent, platform, sessionData, isTest, isMovSupported,
} = initialArgs;
const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
// eslint-disable-next-line no-restricted-globals
(self as any).isMovSupported = isMovSupported;
client = new TelegramClient(
session,
process.env.TELEGRAM_T_API_ID,
process.env.TELEGRAM_T_API_HASH,
{
deviceModel: navigator.userAgent || userAgent || DEFAULT_USER_AGENT,
systemVersion: platform || DEFAULT_PLATFORM,
appVersion: `${APP_VERSION} ${APP_CODE_NAME}`,
useWSS: true,
additionalDcsDisabled: IS_TEST,
testServers: isTest,
} as any,
);
client.addEventHandler(handleGramJsUpdate, gramJsUpdateEventBuilder);
client.addEventHandler(updater, gramJsUpdateEventBuilder);
try {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('[GramJs/client] CONNECTING');
// eslint-disable-next-line no-restricted-globals
(self as any).invoke = invokeRequest;
// eslint-disable-next-line no-restricted-globals
(self as any).GramJs = GramJs;
}
try {
await client.start({
phoneNumber: onRequestPhoneNumber,
phoneCode: onRequestCode,
password: onRequestPassword,
firstAndLastNames: onRequestRegistration,
qrCode: onRequestQrCode,
onError: onAuthError,
initialMethod: platform === 'iOS' || platform === 'Android' ? 'phoneNumber' : 'qrCode',
});
} catch (err) {
// TODO Investigate which request causes this exception
if (err.message !== 'Disconnect') {
onUpdate({
'@type': 'updateConnectionState',
connectionState: 'connectionStateBroken',
});
return;
}
}
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('>>> FINISH INIT API');
// eslint-disable-next-line no-console
console.log('[GramJs/client] CONNECTED');
}
onAuthReady();
onSessionUpdate(session.getSessionData());
onUpdate({ '@type': 'updateApiReady' });
void fetchCurrentUser();
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log('[GramJs/client] CONNECTING ERROR', err);
}
throw err;
}
}
export async function destroy() {
await invokeRequest(new GramJs.auth.LogOut());
await client.destroy();
}
export async function disconnect() {
await client.disconnect();
}
export function getClient() {
return client;
}
function onSessionUpdate(sessionData: ApiSessionData) {
onUpdate({
'@type': 'updateSession',
sessionData,
});
}
function handleGramJsUpdate(update: any) {
if (update instanceof connection.UpdateConnectionState) {
isConnected = update.state === connection.UpdateConnectionState.connected;
} else if (update instanceof GramJs.UpdatesTooLong) {
void handleTerminatedSession();
} else if (update instanceof connection.UpdateServerTimeOffset) {
onUpdate({
'@type': 'updateServerTimeOffset',
serverTimeOffset: update.timeOffset,
});
}
}
export async function invokeRequest<T extends GramJs.AnyRequest>(
request: T,
shouldHandleUpdates = false,
shouldThrow = false,
): Promise<T['__response'] | undefined> {
if (!isConnected) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.warn(`[GramJs/client] INVOKE ERROR ${request.className}: Client is not connected`);
}
return undefined;
}
try {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log(`[GramJs/client] INVOKE ${request.className}`);
}
const result = await client.invoke(request);
if (DEBUG) {
// eslint-disable-next-line no-console
console.log(`[GramJs/client] INVOKE RESPONSE ${request.className}`, result);
}
if (shouldHandleUpdates) {
type ResultWithUpdates =
typeof result
& { updates?: GramJs.Updates | GramJs.UpdatesCombined };
let updatesContainer;
if (result instanceof GramJs.Updates || result instanceof GramJs.UpdatesCombined) {
updatesContainer = result;
} else if ('updates' in (result as ResultWithUpdates) && (
(result as ResultWithUpdates).updates instanceof GramJs.Updates
|| (result as ResultWithUpdates).updates instanceof GramJs.UpdatesCombined
)) {
updatesContainer = (result as ResultWithUpdates).updates;
}
if (updatesContainer) {
injectUpdateEntities(updatesContainer);
updatesContainer.updates.forEach((update) => {
updater(update, request);
});
} else if (result instanceof GramJs.UpdatesTooLong) {
// TODO Implement
} else {
updater(result as GramJs.TypeUpdates, request);
}
}
return result;
} catch (err) {
if (DEBUG) {
// eslint-disable-next-line no-console
console.log(`[GramJs/client] INVOKE ERROR ${request.className}`);
// eslint-disable-next-line no-console
console.error(err);
}
if (shouldThrow) {
throw err;
}
dispatchErrorUpdate(err, request);
return undefined;
}
}
export function downloadMedia(
args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean },
onProgress?: ApiOnProgress,
) {
return downloadMediaWithClient(args, client, isConnected, onProgress);
}
export function uploadFile(file: File, onProgress?: ApiOnProgress) {
return client.uploadFile({ file, onProgress, workers: UPLOAD_WORKERS });
}
export function updateTwoFaSettings(params: TwoFaParams) {
return client.updateTwoFaSettings(params);
}
export async function fetchCurrentUser() {
const userFull = await invokeRequest(new GramJs.users.GetFullUser({
id: new GramJs.InputUserSelf(),
}));
if (!userFull || !(userFull.user instanceof GramJs.User)) {
return;
}
localDb.users[buildApiPeerId(userFull.user.id, 'user')] = userFull.user;
const currentUser = buildApiUserFromFull(userFull);
setMessageBuilderCurrentUserId(currentUser.id);
onCurrentUserUpdate(currentUser);
}
export function dispatchErrorUpdate<T extends GramJs.AnyRequest>(err: Error, request: T) {
const isSlowMode = err.message.startsWith('A wait of') && (
request instanceof GramJs.messages.SendMessage
|| request instanceof GramJs.messages.SendMedia
|| request instanceof GramJs.messages.SendMultiMedia
);
const { message } = err;
onUpdate({
'@type': 'error',
error: {
message,
isSlowMode,
hasErrorKey: true,
},
});
}
function injectUpdateEntities(result: GramJs.Updates | GramJs.UpdatesCombined) {
const entities = [...result.users, ...result.chats];
result.updates.forEach((update) => {
if (entities) {
// eslint-disable-next-line no-underscore-dangle
(update as any)._entities = entities;
}
});
}
async function handleTerminatedSession() {
try {
await invokeRequest(new GramJs.users.GetFullUser({
id: new GramJs.InputUserSelf(),
}), undefined, true);
} catch (err) {
if (err.message === 'AUTH_KEY_UNREGISTERED') {
onUpdate({
'@type': 'updateConnectionState',
connectionState: 'connectionStateBroken',
});
}
}
}