Migrate GramJS to TypeScript (#5364)
This commit is contained in:
parent
5381c148e4
commit
d1304c621d
@ -21,8 +21,8 @@
|
||||
"check": "tsc && stylelint \"**/*.{css,scss}\" && eslint . --ext .ts,.tsx,.js --ignore-pattern src/lib/gramjs",
|
||||
"check:fix": "npm run check -- --fix",
|
||||
"tl:rehash": "node ./dev/tlHash.js",
|
||||
"gramjs:tl": "node ./src/lib/gramjs/tl/generateModules.js",
|
||||
"gramjs:lint": "eslint src/lib/gramjs --ext .ts,.tsx,.js",
|
||||
"gramjs:tl": "tsx ./src/lib/gramjs/tl/generateModules.ts",
|
||||
"gramjs:lint": "cd src/lib/gramjs && eslint --ext .ts,.tsx,.js .",
|
||||
"gramjs:lint:fix": "npm run gramjs:lint -- --fix",
|
||||
"lang:ts": "tsx ./dev/generateLangTypes.js",
|
||||
"lang:initial": "tsx ./dev/generateInitialLangFallback.js",
|
||||
|
||||
@ -221,7 +221,7 @@ function buildStatisticsOverview({ current, previous }: GramJs.StatsAbsValueAndP
|
||||
return {
|
||||
current,
|
||||
change,
|
||||
...(previous && { percentage: (change ? ((Math.abs(change) / previous) * 100) : 0).toFixed(2) }),
|
||||
percentage: (change ? ((Math.abs(change) / previous) * 100) : 0).toFixed(2),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import BigInt from 'big-integer';
|
||||
import { constructors } from '../../lib/gramjs/tl';
|
||||
|
||||
import type { Api as GramJs } from '../../lib/gramjs';
|
||||
import { Api as GramJs } from '../../lib/gramjs';
|
||||
|
||||
import { DATA_BROADCAST_CHANNEL_NAME, DEBUG } from '../../config';
|
||||
import { throttle } from '../../util/schedulers';
|
||||
@ -82,7 +80,7 @@ function convertToVirtualClass(value: any): any {
|
||||
const path = value.className.split('.');
|
||||
const VirtualClass = path.reduce((acc: any, field: string) => {
|
||||
return acc[field];
|
||||
}, constructors);
|
||||
}, GramJs);
|
||||
|
||||
const valueOmited = omitVirtualClassFields(value);
|
||||
const valueConverted = Object.keys(valueOmited).reduce((acc, key) => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { errors } from '../../../lib/gramjs';
|
||||
import { FloodWaitError, RPCError } from '../../../lib/gramjs/errors';
|
||||
|
||||
import type {
|
||||
ApiUpdateAuthorizationState,
|
||||
@ -87,11 +87,13 @@ export function onRequestQrCode(qrCode: { token: Buffer; expires: number }) {
|
||||
export function onAuthError(err: Error) {
|
||||
let message: string;
|
||||
|
||||
if (err instanceof errors.FloodWaitError) {
|
||||
if (err instanceof FloodWaitError) {
|
||||
const hours = Math.ceil(Number(err.seconds) / 60 / 60);
|
||||
message = `Too many attempts. Try again in ${hours > 1 ? `${hours} hours` : 'an hour'}`;
|
||||
} else if (err instanceof RPCError) {
|
||||
message = ApiErrors[err.errorMessage];
|
||||
} else {
|
||||
message = ApiErrors[err.message];
|
||||
message = err.message;
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { RPCError } from '../../../lib/gramjs/errors';
|
||||
|
||||
import type {
|
||||
ApiChat,
|
||||
@ -1460,11 +1461,12 @@ export async function addChatMembers(chat: ApiChat, users: ApiUser[]) {
|
||||
if (addChatUsersResult) {
|
||||
return addChatUsersResult.flat().filter(Boolean);
|
||||
}
|
||||
} catch (err) {
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof RPCError ? err.errorMessage : (err as Error).message;
|
||||
sendApiUpdate({
|
||||
'@type': 'error',
|
||||
error: {
|
||||
message: (err as Error).message,
|
||||
message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import {
|
||||
Api as GramJs,
|
||||
sessions,
|
||||
type Update,
|
||||
} from '../../../lib/gramjs';
|
||||
import type { TwoFaParams, TwoFaPasswordParams } from '../../../lib/gramjs/client/2fa';
|
||||
import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
|
||||
import { RPCError } from '../../../lib/gramjs/errors';
|
||||
import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index';
|
||||
|
||||
import type { ThreadId } from '../../../types';
|
||||
@ -54,7 +56,7 @@ const DEFAULT_PLATFORM = 'Unknown platform';
|
||||
|
||||
GramJsLogger.setLevel(DEBUG_GRAMJS ? 'debug' : 'warn');
|
||||
|
||||
const gramJsUpdateEventBuilder = { build: (update: object) => update };
|
||||
const gramJsUpdateEventBuilder = { build: (update: Update) => update };
|
||||
|
||||
const CHAT_ABORT_CONTROLLERS = new Map<string, ChatAbortController>();
|
||||
const ABORT_CONTROLLERS = new Map<string, AbortController>();
|
||||
@ -83,7 +85,7 @@ export async function init(initialArgs: ApiInitialArgs) {
|
||||
|
||||
client = new TelegramClient(
|
||||
session,
|
||||
process.env.TELEGRAM_API_ID,
|
||||
Number(process.env.TELEGRAM_API_ID),
|
||||
process.env.TELEGRAM_API_HASH,
|
||||
{
|
||||
deviceModel: navigator.userAgent || userAgent || DEFAULT_USER_AGENT,
|
||||
@ -194,7 +196,7 @@ export function getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
function onSessionUpdate(sessionData: ApiSessionData) {
|
||||
function onSessionUpdate(sessionData?: ApiSessionData) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateSession',
|
||||
sessionData,
|
||||
@ -330,27 +332,29 @@ export async function downloadMedia(
|
||||
) {
|
||||
try {
|
||||
return (await downloadMediaWithClient(args, client, onProgress));
|
||||
} catch (err: any) {
|
||||
if (err.message.startsWith('FILE_REFERENCE')) {
|
||||
const isFileReferenceRepaired = await repairFileReference({ url: args.url });
|
||||
if (isFileReferenceRepaired) {
|
||||
return downloadMediaWithClient(args, client, onProgress);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError) {
|
||||
if (err.errorMessage.startsWith('FILE_REFERENCE')) {
|
||||
const isFileReferenceRepaired = await repairFileReference({ url: args.url });
|
||||
if (isFileReferenceRepaired) {
|
||||
return downloadMediaWithClient(args, client, onProgress);
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to repair file reference', args.url);
|
||||
}
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to repair file reference', args.url);
|
||||
if (err.errorMessage === 'FILE_ID_INVALID' && args.url.includes('avatar')) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Inaccessible avatar image', args.url);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (err.message === 'FILE_ID_INVALID' && args.url.includes('avatar')) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Inaccessible avatar image', args.url);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to download media', args.url, err);
|
||||
@ -416,14 +420,13 @@ export async function fetchCurrentUser() {
|
||||
}
|
||||
|
||||
export function dispatchErrorUpdate<T extends GramJs.AnyRequest>(err: Error, request: T) {
|
||||
const isSlowMode = err.message.startsWith('A wait of') && (
|
||||
const message = err instanceof RPCError ? err.errorMessage : err.message;
|
||||
const isSlowMode = message === 'FLOOD' && (
|
||||
request instanceof GramJs.messages.SendMessage
|
||||
|| request instanceof GramJs.messages.SendMedia
|
||||
|| request instanceof GramJs.messages.SendMultiMedia
|
||||
);
|
||||
|
||||
const { message } = err;
|
||||
|
||||
sendApiUpdate({
|
||||
'@type': 'error',
|
||||
error: {
|
||||
@ -442,7 +445,7 @@ async function handleTerminatedSession() {
|
||||
shouldThrow: true,
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err.message === 'AUTH_KEY_UNREGISTERED' || err.message === 'SESSION_REVOKED') {
|
||||
if (err.errorMessage === 'AUTH_KEY_UNREGISTERED' || err.errorMessage === 'SESSION_REVOKED') {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateConnectionState',
|
||||
connectionState: 'connectionStateBroken',
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import bigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type { TelegramClient } from '../../../lib/gramjs';
|
||||
import type { SizeType, TelegramClient } from '../../../lib/gramjs';
|
||||
import type { ApiOnProgress, ApiParsedMedia } from '../../types';
|
||||
import {
|
||||
ApiMediaFormat,
|
||||
@ -88,15 +89,16 @@ async function download(
|
||||
} = parsed;
|
||||
|
||||
if (entityType === 'staticMap') {
|
||||
const accessHash = entityId;
|
||||
const accessHash = bigInt(entityId);
|
||||
const parsedParams = new URLSearchParams(params);
|
||||
const long = parsedParams.get('long');
|
||||
const lat = parsedParams.get('lat');
|
||||
const w = parsedParams.get('w');
|
||||
const h = parsedParams.get('h');
|
||||
const zoom = parsedParams.get('zoom');
|
||||
const scale = parsedParams.get('scale');
|
||||
const accuracyRadius = parsedParams.get('accuracy_radius');
|
||||
const long = Number(parsedParams.get('long'));
|
||||
const lat = Number(parsedParams.get('lat'));
|
||||
const w = Number(parsedParams.get('w'));
|
||||
const h = Number(parsedParams.get('h'));
|
||||
const zoom = Number(parsedParams.get('zoom'));
|
||||
const scale = Number(parsedParams.get('scale'));
|
||||
const accuracyRadiusStr = parsedParams.get('accuracy_radius');
|
||||
const accuracyRadius = accuracyRadiusStr ? Number(accuracyRadiusStr) : undefined;
|
||||
|
||||
const data = await client.downloadStaticMap(accessHash, long, lat, w, h, zoom, scale, accuracyRadius);
|
||||
return {
|
||||
@ -167,13 +169,13 @@ async function download(
|
||||
|
||||
return { mimeType, data, fullSize };
|
||||
} else if (entityType === 'stickerSet') {
|
||||
const data = await client.downloadStickerSetThumb(entity);
|
||||
const mimeType = getMimeType(data);
|
||||
const data = await client.downloadStickerSetThumb(entity as GramJs.StickerSet);
|
||||
const mimeType = data && getMimeType(data);
|
||||
|
||||
return { mimeType, data };
|
||||
} else {
|
||||
const data = await client.downloadProfilePhoto(entity, mediaMatchType === 'profile');
|
||||
const mimeType = getMimeType(data);
|
||||
const data = await client.downloadProfilePhoto(entity as GramJs.Chat | GramJs.User, mediaMatchType === 'profile');
|
||||
const mimeType = data && getMimeType(data);
|
||||
|
||||
return { mimeType, data };
|
||||
}
|
||||
@ -181,8 +183,12 @@ async function download(
|
||||
|
||||
// eslint-disable-next-line no-async-without-await/no-async-without-await
|
||||
async function parseMedia(
|
||||
data: Buffer, mediaFormat: ApiMediaFormat, mimeType?: string,
|
||||
data: Buffer | File, mediaFormat: ApiMediaFormat, mimeType?: string,
|
||||
): Promise<ApiParsedMedia | undefined> {
|
||||
if (data instanceof File) {
|
||||
return data;
|
||||
}
|
||||
|
||||
switch (mediaFormat) {
|
||||
case ApiMediaFormat.BlobUrl:
|
||||
return new Blob([data], { type: mimeType });
|
||||
@ -246,7 +252,7 @@ export function parseMediaUrl(url: string) {
|
||||
|
||||
let entityType: EntityType;
|
||||
const params = mediaMatch[3];
|
||||
const sizeType = params?.replace('?size=', '') || undefined;
|
||||
const sizeType = params?.replace('?size=', '') as SizeType || undefined;
|
||||
|
||||
if (mediaMatch[1] === 'avatar' || mediaMatch[1] === 'profile') {
|
||||
entityType = getEntityTypeById(entityId);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { RPCError } from '../../../lib/gramjs/errors';
|
||||
|
||||
import type { ThreadId, WebPageMediaSize } from '../../../types';
|
||||
import type {
|
||||
@ -153,7 +154,7 @@ export async function fetchMessages({
|
||||
abortControllerThreadId: threadId,
|
||||
});
|
||||
} catch (err: any) {
|
||||
if (err.message === 'CHANNEL_PRIVATE') {
|
||||
if (err.errorMessage === 'CHANNEL_PRIVATE') {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: chat.id,
|
||||
@ -421,7 +422,7 @@ export function sendMessage(
|
||||
});
|
||||
if (update) handleLocalMessageUpdate(localMessage, update);
|
||||
} catch (error: any) {
|
||||
if (error.message === 'PRIVACY_PREMIUM_REQUIRED') {
|
||||
if (error.errorMessage === 'PRIVACY_PREMIUM_REQUIRED') {
|
||||
sendApiUpdate({ '@type': 'updateRequestUserUpdate', id: chat.id });
|
||||
}
|
||||
|
||||
@ -1786,8 +1787,8 @@ export async function reportSponsoredMessage({
|
||||
}
|
||||
|
||||
return buildApiSponsoredMessageReportResult(result);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message === 'PREMIUM_ACCOUNT_REQUIRED') {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'PREMIUM_ACCOUNT_REQUIRED') {
|
||||
return {
|
||||
type: 'premiumRequired' as const,
|
||||
};
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type bigInt from 'big-integer';
|
||||
import BigInt from 'big-integer';
|
||||
import AuthKey from '../../../lib/gramjs/crypto/AuthKey';
|
||||
import Logger from '../../../lib/gramjs/extensions/Logger';
|
||||
import Helpers from '../../../lib/gramjs/Helpers';
|
||||
import { AuthKey } from '../../../lib/gramjs/crypto/AuthKey';
|
||||
import { Logger } from '../../../lib/gramjs/extensions';
|
||||
import {
|
||||
convertToLittle, getByteArray, modExp, readBigIntFromBuffer, sha1, sha256,
|
||||
} from '../../../lib/gramjs/Helpers';
|
||||
import MTProtoState from '../../../lib/gramjs/network/MTProtoState';
|
||||
|
||||
type DhConfig = {
|
||||
@ -39,58 +41,62 @@ class PhoneCallState {
|
||||
}
|
||||
|
||||
async requestCall({ p, g, random }: DhConfig) {
|
||||
const pBN = Helpers.readBigIntFromBuffer(Buffer.from(p), false);
|
||||
const randomBN = Helpers.readBigIntFromBuffer(Buffer.from(random), false);
|
||||
const pBN = readBigIntFromBuffer(Buffer.from(p), false);
|
||||
const randomBN = readBigIntFromBuffer(Buffer.from(random), false);
|
||||
|
||||
const gA = Helpers.modExp(BigInt(g), randomBN, pBN);
|
||||
const gA = modExp(BigInt(g), randomBN, pBN);
|
||||
|
||||
this.gA = gA;
|
||||
this.p = pBN;
|
||||
this.random = randomBN;
|
||||
|
||||
const gAHash: Buffer = await Helpers.sha256(Helpers.getByteArray(gA));
|
||||
const gAHash: Buffer = await sha256(getByteArray(gA));
|
||||
return Array.from(gAHash);
|
||||
}
|
||||
|
||||
acceptCall({ p, g, random }: DhConfig) {
|
||||
const pLast = Helpers.readBigIntFromBuffer(p, false);
|
||||
const randomLast = Helpers.readBigIntFromBuffer(random, false);
|
||||
const pLast = readBigIntFromBuffer(p, false);
|
||||
const randomLast = readBigIntFromBuffer(random, false);
|
||||
|
||||
const gB = Helpers.modExp(BigInt(g), randomLast, pLast);
|
||||
const gB = modExp(BigInt(g), randomLast, pLast);
|
||||
this.gB = gB;
|
||||
this.p = pLast;
|
||||
this.random = randomLast;
|
||||
|
||||
return Array.from(Helpers.getByteArray(gB));
|
||||
return Array.from(getByteArray(gB));
|
||||
}
|
||||
|
||||
async confirmCall(gAOrB: number[], emojiData: Uint16Array, emojiOffsets: number[]) {
|
||||
if (this.isOutgoing) {
|
||||
this.gB = Helpers.readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
||||
} else {
|
||||
this.gA = Helpers.readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
||||
if (!this.random || !this.p) {
|
||||
throw new Error('Values not set');
|
||||
}
|
||||
const authKey = Helpers.modExp(
|
||||
|
||||
if (this.isOutgoing) {
|
||||
this.gB = readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
||||
} else {
|
||||
this.gA = readBigIntFromBuffer(Buffer.from(gAOrB), false);
|
||||
}
|
||||
const authKey = modExp(
|
||||
!this.isOutgoing ? this.gA : this.gB,
|
||||
this.random,
|
||||
this.p,
|
||||
);
|
||||
const fingerprint: Buffer = await Helpers.sha1(Helpers.getByteArray(authKey));
|
||||
const keyFingerprint = Helpers.readBigIntFromBuffer(fingerprint.slice(-8).reverse(), false);
|
||||
const fingerprint: Buffer = await sha1(getByteArray(authKey));
|
||||
const keyFingerprint = readBigIntFromBuffer(fingerprint.slice(-8).reverse(), false);
|
||||
|
||||
const emojis = await generateEmojiFingerprint(
|
||||
Helpers.getByteArray(authKey),
|
||||
Helpers.getByteArray(this.gA),
|
||||
getByteArray(authKey),
|
||||
getByteArray(this.gA!),
|
||||
emojiData,
|
||||
emojiOffsets,
|
||||
);
|
||||
|
||||
const key = new AuthKey();
|
||||
await key.setKey(Helpers.getByteArray(authKey));
|
||||
await key.setKey(getByteArray(authKey));
|
||||
this.state = new MTProtoState(key, new Logger(), true, this.isOutgoing);
|
||||
this.resolveState!();
|
||||
|
||||
return { gA: Array.from(Helpers.getByteArray(this.gA)), keyFingerprint: keyFingerprint.toString(), emojis };
|
||||
return { gA: Array.from(getByteArray(this.gA!)), keyFingerprint: keyFingerprint.toString(), emojis };
|
||||
}
|
||||
|
||||
async encode(data: string) {
|
||||
@ -99,7 +105,7 @@ class PhoneCallState {
|
||||
const seqArray = new Uint32Array(1);
|
||||
seqArray[0] = this.seq++;
|
||||
const encodedData = await this.state.encryptMessageData(
|
||||
Buffer.concat([Helpers.convertToLittle(seqArray), Buffer.from(data)]),
|
||||
Buffer.concat([convertToLittle(seqArray), Buffer.from(data)]),
|
||||
);
|
||||
return Array.from(encodedData);
|
||||
}
|
||||
@ -132,7 +138,7 @@ function computeEmojiIndex(bytes: Uint8Array) {
|
||||
async function generateEmojiFingerprint(
|
||||
authKey: Uint8Array, gA: Uint8Array, emojiData: Uint16Array, emojiOffsets: number[],
|
||||
) {
|
||||
const hash = await Helpers.sha256(Buffer.concat([new Uint8Array(authKey), new Uint8Array(gA)]));
|
||||
const hash = await sha256(Buffer.concat([new Uint8Array(authKey), new Uint8Array(gA)]));
|
||||
const result = [];
|
||||
const emojiCount = emojiOffsets.length - 1;
|
||||
const kPartSize = 8;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { RPCError } from '../../../lib/gramjs/errors';
|
||||
|
||||
import type { LANG_PACKS } from '../../../config';
|
||||
import type {
|
||||
ApiAppConfig,
|
||||
ApiConfig,
|
||||
ApiError,
|
||||
ApiInputPrivacyRules,
|
||||
ApiLanguage,
|
||||
ApiNotifyException,
|
||||
@ -77,17 +77,15 @@ export async function checkUsername(username: string) {
|
||||
});
|
||||
|
||||
return { result, error: undefined };
|
||||
} catch (error) {
|
||||
const errorMessage = (error as ApiError).message;
|
||||
|
||||
if (ACCEPTABLE_USERNAME_ERRORS.has(errorMessage)) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && ACCEPTABLE_USERNAME_ERRORS.has(err.errorMessage)) {
|
||||
return {
|
||||
result: false,
|
||||
error: errorMessage,
|
||||
error: err.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Api as GramJs, connection } from '../../../lib/gramjs';
|
||||
import { Api as GramJs, type Update } from '../../../lib/gramjs';
|
||||
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gramjs/network';
|
||||
|
||||
import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
|
||||
import type {
|
||||
@ -81,33 +82,29 @@ import { sendApiUpdate } from './apiUpdateEmitter';
|
||||
import { processMessageAndUpdateThreadInfo } from './entityProcessor';
|
||||
|
||||
import LocalUpdatePremiumFloodWait from './UpdatePremiumFloodWait';
|
||||
import { LocalUpdateChannelPts, LocalUpdatePts, type UpdatePts } from './UpdatePts';
|
||||
|
||||
export type Update = (
|
||||
(GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] }
|
||||
) | typeof connection.UpdateConnectionState | UpdatePts | LocalUpdatePremiumFloodWait;
|
||||
import { LocalUpdateChannelPts, LocalUpdatePts } from './UpdatePts';
|
||||
|
||||
const sentMessageIds = new Set();
|
||||
|
||||
export function updater(update: Update) {
|
||||
if (update instanceof connection.UpdateServerTimeOffset) {
|
||||
if (update instanceof UpdateServerTimeOffset) {
|
||||
setServerTimeOffset(update.timeOffset);
|
||||
|
||||
sendApiUpdate({
|
||||
'@type': 'updateServerTimeOffset',
|
||||
serverTimeOffset: update.timeOffset,
|
||||
});
|
||||
} else if (update instanceof connection.UpdateConnectionState) {
|
||||
} else if (update instanceof UpdateConnectionState) {
|
||||
let connectionState: ApiUpdateConnectionStateType;
|
||||
|
||||
switch (update.state) {
|
||||
case connection.UpdateConnectionState.disconnected:
|
||||
case UpdateConnectionState.disconnected:
|
||||
connectionState = 'connectionStateConnecting';
|
||||
break;
|
||||
case connection.UpdateConnectionState.broken:
|
||||
case UpdateConnectionState.broken:
|
||||
connectionState = 'connectionStateBroken';
|
||||
break;
|
||||
case connection.UpdateConnectionState.connected:
|
||||
case UpdateConnectionState.connected:
|
||||
default:
|
||||
connectionState = 'connectionStateReady';
|
||||
break;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { Api as GramJs, type Update } from '../../../lib/gramjs';
|
||||
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gramjs/network';
|
||||
|
||||
import type { ApiChat } from '../../types';
|
||||
@ -11,7 +11,7 @@ import { buildInputEntity, buildMtpPeerId } from '../gramjsBuilders';
|
||||
import localDb from '../localDb';
|
||||
import { sendApiUpdate } from './apiUpdateEmitter';
|
||||
import { processAndUpdateEntities } from './entityProcessor';
|
||||
import { type Update, updater } from './mtpUpdateHandler';
|
||||
import { updater } from './mtpUpdateHandler';
|
||||
|
||||
import { buildLocalUpdatePts, type UpdatePts } from './UpdatePts';
|
||||
|
||||
|
||||
@ -24,7 +24,7 @@ export interface ApiPhotoSize extends ApiDimensions {
|
||||
|
||||
export interface ApiVideoSize extends ApiDimensions {
|
||||
type: 'u' | 'v';
|
||||
videoStartTs: number;
|
||||
videoStartTs?: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
|
||||
@ -101,9 +101,9 @@ export interface ApiWebSession {
|
||||
|
||||
export interface ApiSessionData {
|
||||
mainDcId: number;
|
||||
isTest?: true;
|
||||
keys: Record<number, string | number[]>;
|
||||
hashes: Record<number, string | number[]>;
|
||||
isTest?: true;
|
||||
}
|
||||
|
||||
export type ApiNotifyException = {
|
||||
|
||||
@ -2173,7 +2173,7 @@ addActionHandler('toggleForum', async (global, actions, payload): Promise<void>
|
||||
try {
|
||||
result = await callApi('toggleForum', { chat, isEnabled });
|
||||
} catch (error) {
|
||||
if ((error as ApiError).message.startsWith('A wait of')) {
|
||||
if ((error as ApiError).message === 'FLOOD') {
|
||||
actions.showNotification({ message: langProvider.oldTranslate('FloodWait'), tabId });
|
||||
} else {
|
||||
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
|
||||
|
||||
@ -577,7 +577,7 @@ async function loadStickers<T extends GlobalState>(
|
||||
'fetchStickers',
|
||||
{ stickerSetInfo },
|
||||
);
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
if ((error as ApiError).message === 'STICKERSET_INVALID') {
|
||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||
actions.showNotification({
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
src/lib/gramjs/tl/types-generator/template.js
|
||||
src/lib/gramjs/tl/api.d.ts
|
||||
src/lib/gramjs/tl/apiTl.js
|
||||
src/lib/gramjs/tl/schemaTl.js
|
||||
src/lib/gramjs/tl/apiTl.full.js
|
||||
src/lib/gramjs/tl/schemaTl.full.js
|
||||
tl/api.d.ts
|
||||
tl/apiTl.ts
|
||||
tl/schemaTl.ts
|
||||
tl/apiTl.full.ts
|
||||
tl/schemaTl.full.ts
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"root": true,
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
|
||||
@ -1,345 +0,0 @@
|
||||
const BigInt = require('big-integer');
|
||||
const crypto = require('./crypto/crypto');
|
||||
|
||||
/**
|
||||
* converts a buffer to big int
|
||||
* @param buffer
|
||||
* @param little
|
||||
* @param signed
|
||||
* @returns {bigInt.BigInteger}
|
||||
*/
|
||||
function readBigIntFromBuffer(buffer, little = true, signed = false) {
|
||||
let randBuffer = Buffer.from(buffer);
|
||||
const bytesNumber = randBuffer.length;
|
||||
if (little) {
|
||||
randBuffer = randBuffer.reverse();
|
||||
}
|
||||
let bigInt = BigInt(randBuffer.toString('hex'), 16);
|
||||
if (signed && Math.floor(bigInt.toString(2).length / 8) >= bytesNumber) {
|
||||
bigInt = bigInt.subtract(BigInt(2)
|
||||
.pow(BigInt(bytesNumber * 8)));
|
||||
}
|
||||
return bigInt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Special case signed little ints
|
||||
* @param big
|
||||
* @param number
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function toSignedLittleBuffer(big, number = 8) {
|
||||
const bigNumber = BigInt(big);
|
||||
const byteArray = [];
|
||||
for (let i = 0; i < number; i++) {
|
||||
byteArray[i] = bigNumber.shiftRight(8 * i)
|
||||
.and(255);
|
||||
}
|
||||
return Buffer.from(byteArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* converts a big int to a buffer
|
||||
* @param bigInt {bigInt.BigInteger}
|
||||
* @param bytesNumber
|
||||
* @param little
|
||||
* @param signed
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function readBufferFromBigInt(bigInt, bytesNumber, little = true, signed = false) {
|
||||
bigInt = BigInt(bigInt);
|
||||
const bitLength = bigInt.bitLength().toJSNumber();
|
||||
|
||||
const bytes = Math.ceil(bitLength / 8);
|
||||
if (bytesNumber < bytes) {
|
||||
throw new Error('OverflowError: int too big to convert');
|
||||
}
|
||||
if (!signed && bigInt.lesser(BigInt(0))) {
|
||||
throw new Error('Cannot convert to unsigned');
|
||||
}
|
||||
let below = false;
|
||||
if (bigInt.lesser(BigInt(0))) {
|
||||
below = true;
|
||||
bigInt = bigInt.abs();
|
||||
}
|
||||
|
||||
const hex = bigInt.toString(16).padStart(bytesNumber * 2, '0');
|
||||
let buffer = Buffer.from(hex, 'hex');
|
||||
|
||||
if (signed && below) {
|
||||
buffer[buffer.length - 1] = 256 - buffer[buffer.length - 1];
|
||||
for (let i = 0; i < buffer.length - 1; i++) {
|
||||
buffer[i] = 255 - buffer[i];
|
||||
}
|
||||
}
|
||||
if (little) {
|
||||
buffer = buffer.reverse();
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random long integer (8 bytes), which is optionally signed
|
||||
* @returns {BigInteger}
|
||||
*/
|
||||
function generateRandomLong(signed = true) {
|
||||
return readBigIntFromBuffer(generateRandomBytes(8), true, signed);
|
||||
}
|
||||
|
||||
/**
|
||||
* .... really javascript
|
||||
* @param n {number}
|
||||
* @param m {number}
|
||||
* @returns {number}
|
||||
*/
|
||||
function mod(n, m) {
|
||||
return ((n % m) + m) % m;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a positive bigInt
|
||||
* @param n {BigInt}
|
||||
* @param m {BigInt}
|
||||
* @returns {BigInt}
|
||||
*/
|
||||
function bigIntMod(n, m) {
|
||||
return ((n.remainder(m)).add(m)).remainder(m);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a random bytes array
|
||||
* @param count
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function generateRandomBytes(count) {
|
||||
return Buffer.from(crypto.randomBytes(count));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the key based on Telegram guidelines, specifying whether it's the client or not
|
||||
* @param sharedKey
|
||||
* @param msgKey
|
||||
* @param client
|
||||
* @returns {{iv: Buffer, key: Buffer}}
|
||||
*/
|
||||
|
||||
/* CONTEST
|
||||
this is mtproto 1 (mostly used for secret chats)
|
||||
async function calcKey(sharedKey, msgKey, client) {
|
||||
const x = client === true ? 0 : 8
|
||||
const [sha1a, sha1b, sha1c, sha1d] = await Promise.all([
|
||||
sha1(Buffer.concat([msgKey, sharedKey.slice(x, x + 32)])),
|
||||
sha1(Buffer.concat([sharedKey.slice(x + 32, x + 48), msgKey, sharedKey.slice(x + 48, x + 64)])),
|
||||
sha1(Buffer.concat([sharedKey.slice(x + 64, x + 96), msgKey])),
|
||||
sha1(Buffer.concat([msgKey, sharedKey.slice(x + 96, x + 128)]))
|
||||
])
|
||||
const key = Buffer.concat([sha1a.slice(0, 8), sha1b.slice(8, 20), sha1c.slice(4, 16)])
|
||||
const iv = Buffer.concat([sha1a.slice(8, 20), sha1b.slice(0, 8), sha1c.slice(16, 20), sha1d.slice(0, 8)])
|
||||
return {
|
||||
key,
|
||||
iv
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Generates the key data corresponding to the given nonces
|
||||
* @param serverNonce
|
||||
* @param newNonce
|
||||
* @returns {{key: Buffer, iv: Buffer}}
|
||||
*/
|
||||
async function generateKeyDataFromNonce(serverNonce, newNonce) {
|
||||
serverNonce = toSignedLittleBuffer(serverNonce, 16);
|
||||
newNonce = toSignedLittleBuffer(newNonce, 32);
|
||||
const [hash1, hash2, hash3] = await Promise.all([
|
||||
sha1(Buffer.concat([newNonce, serverNonce])),
|
||||
sha1(Buffer.concat([serverNonce, newNonce])),
|
||||
sha1(Buffer.concat([newNonce, newNonce])),
|
||||
]);
|
||||
const keyBuffer = Buffer.concat([hash1, hash2.slice(0, 12)]);
|
||||
const ivBuffer = Buffer.concat([hash2.slice(12, 20), hash3, newNonce.slice(0, 4)]);
|
||||
return {
|
||||
key: keyBuffer,
|
||||
iv: ivBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
function convertToLittle(buf) {
|
||||
const correct = Buffer.alloc(buf.length * 4);
|
||||
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
correct.writeUInt32BE(buf[i], i * 4);
|
||||
}
|
||||
return correct;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the SHA1 digest for the given data
|
||||
* @param data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function sha1(data) {
|
||||
const shaSum = crypto.createHash('sha1');
|
||||
shaSum.update(data);
|
||||
return shaSum.digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the SHA256 digest for the given data
|
||||
* @param data
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function sha256(data) {
|
||||
const shaSum = crypto.createHash('sha256');
|
||||
shaSum.update(data);
|
||||
return shaSum.digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast mod pow for RSA calculation. a^b % n
|
||||
* @param a
|
||||
* @param b
|
||||
* @param n
|
||||
* @returns {bigInt.BigInteger}
|
||||
*/
|
||||
function modExp(a, b, n) {
|
||||
a = a.remainder(n);
|
||||
let result = BigInt.one;
|
||||
let x = a;
|
||||
while (b.greater(BigInt.zero)) {
|
||||
const leastSignificantBit = b.remainder(BigInt(2));
|
||||
b = b.divide(BigInt(2));
|
||||
if (leastSignificantBit.eq(BigInt.one)) {
|
||||
result = result.multiply(x);
|
||||
result = result.remainder(n);
|
||||
}
|
||||
x = x.multiply(x);
|
||||
x = x.remainder(n);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the arbitrary-length byte array corresponding to the given integer
|
||||
* @param integer {any}
|
||||
* @param signed {boolean}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function getByteArray(integer, signed = false) {
|
||||
const bits = integer.toString(2).length;
|
||||
const byteLength = Math.floor((bits + 8 - 1) / 8);
|
||||
return readBufferFromBigInt(BigInt(integer), byteLength, false, signed);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a random int from min (inclusive) and max (inclusive)
|
||||
* @param min
|
||||
* @param max
|
||||
* @returns {number}
|
||||
*/
|
||||
function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sleeps a specified amount of time
|
||||
* @param ms time in milliseconds
|
||||
* @returns {Promise}
|
||||
*/
|
||||
const sleep = (ms) => new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper to export two buffers of same length
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
|
||||
function bufferXor(a, b) {
|
||||
const res = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
res.push(a[i] ^ b[i]);
|
||||
}
|
||||
return Buffer.from(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the obj is an array
|
||||
* @param obj
|
||||
* @returns {boolean}
|
||||
*/
|
||||
/*
|
||||
CONTEST
|
||||
we do'nt support array requests anyway
|
||||
function isArrayLike(obj) {
|
||||
if (!obj) return false
|
||||
const l = obj.length
|
||||
if (typeof l != 'number' || l < 0) return false
|
||||
if (Math.floor(l) !== l) return false
|
||||
// fast check
|
||||
if (l > 0 && !(l - 1 in obj)) return false
|
||||
// more complete check (optional)
|
||||
for (let i = 0; i < l; ++i) {
|
||||
if (!(i in obj)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
*/
|
||||
|
||||
// Taken from https://stackoverflow.com/questions/18638900/javascript-crc32/18639999#18639999
|
||||
function makeCRCTable() {
|
||||
let c;
|
||||
const crcTable = [];
|
||||
for (let n = 0; n < 256; n++) {
|
||||
c = n;
|
||||
for (let k = 0; k < 8; k++) {
|
||||
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
|
||||
}
|
||||
crcTable[n] = c;
|
||||
}
|
||||
return crcTable;
|
||||
}
|
||||
|
||||
let crcTable;
|
||||
|
||||
function crc32(buf) {
|
||||
if (!crcTable) {
|
||||
crcTable = makeCRCTable();
|
||||
}
|
||||
if (!Buffer.isBuffer(buf)) {
|
||||
buf = Buffer.from(buf);
|
||||
}
|
||||
let crc = -1;
|
||||
|
||||
for (let index = 0; index < buf.length; index++) {
|
||||
const byte = buf[index];
|
||||
crc = crcTable[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ (-1)) >>> 0;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
readBigIntFromBuffer,
|
||||
readBufferFromBigInt,
|
||||
generateRandomLong,
|
||||
mod,
|
||||
crc32,
|
||||
generateRandomBytes,
|
||||
// calcKey,
|
||||
generateKeyDataFromNonce,
|
||||
sha1,
|
||||
sha256,
|
||||
bigIntMod,
|
||||
modExp,
|
||||
getRandomInt,
|
||||
sleep,
|
||||
getByteArray,
|
||||
// isArrayLike,
|
||||
toSignedLittleBuffer,
|
||||
convertToLittle,
|
||||
bufferXor,
|
||||
};
|
||||
190
src/lib/gramjs/Helpers.ts
Normal file
190
src/lib/gramjs/Helpers.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import BigInt from 'big-integer';
|
||||
|
||||
import { createHash, randomBytes } from './crypto/crypto';
|
||||
|
||||
export function readBigIntFromBuffer(buffer: Buffer | number[], little = true, signed = false): BigInt.BigInteger {
|
||||
let randBuffer = Buffer.from(buffer);
|
||||
const bytesNumber = randBuffer.length;
|
||||
if (little) {
|
||||
randBuffer = randBuffer.reverse();
|
||||
}
|
||||
let bigInt = BigInt(randBuffer.toString('hex'), 16);
|
||||
if (signed && Math.floor(bigInt.toString(2).length / 8) >= bytesNumber) {
|
||||
bigInt = bigInt.subtract(BigInt(2)
|
||||
.pow(BigInt(bytesNumber * 8)));
|
||||
}
|
||||
return bigInt;
|
||||
}
|
||||
|
||||
export function toSignedLittleBuffer(big: BigInt.BigInteger, number = 8) {
|
||||
const bigNumber = BigInt(big);
|
||||
const byteArray: number[] = [];
|
||||
for (let i = 0; i < number; i++) {
|
||||
byteArray[i] = bigNumber.shiftRight(8 * i)
|
||||
.and(255)
|
||||
.toJSNumber();
|
||||
}
|
||||
|
||||
return Buffer.from(byteArray);
|
||||
}
|
||||
|
||||
export function readBufferFromBigInt(bigInt: BigInt.BigInteger, bytesNumber: number, little = true, signed = false) {
|
||||
const bitLength = bigInt.bitLength().toJSNumber();
|
||||
|
||||
const bytes = Math.ceil(bitLength / 8);
|
||||
if (bytesNumber < bytes) {
|
||||
throw new Error('OverflowError: int too big to convert');
|
||||
}
|
||||
if (!signed && bigInt.lesser(BigInt(0))) {
|
||||
throw new Error('Cannot convert to unsigned');
|
||||
}
|
||||
let below = false;
|
||||
if (bigInt.lesser(BigInt(0))) {
|
||||
below = true;
|
||||
bigInt = bigInt.abs();
|
||||
}
|
||||
|
||||
const hex = bigInt.toString(16).padStart(bytesNumber * 2, '0');
|
||||
let buffer = Buffer.from(hex, 'hex');
|
||||
|
||||
if (signed && below) {
|
||||
buffer[buffer.length - 1] = 256 - buffer[buffer.length - 1];
|
||||
for (let i = 0; i < buffer.length - 1; i++) {
|
||||
buffer[i] = 255 - buffer[i];
|
||||
}
|
||||
}
|
||||
if (little) {
|
||||
buffer = buffer.reverse();
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
export function generateRandomLong(signed = true) {
|
||||
return readBigIntFromBuffer(generateRandomBytes(8), true, signed);
|
||||
}
|
||||
|
||||
export function mod(n: number, m: number) {
|
||||
return ((n % m) + m) % m;
|
||||
}
|
||||
|
||||
export function bigIntMod(n: BigInt.BigInteger, m: BigInt.BigInteger) {
|
||||
return ((n.remainder(m)).add(m)).remainder(m);
|
||||
}
|
||||
|
||||
export function generateRandomBytes(count: number) {
|
||||
return Buffer.from(randomBytes(count));
|
||||
}
|
||||
|
||||
export async function generateKeyDataFromNonce(
|
||||
serverNonceBigInt: BigInt.BigInteger, newNonceBigInt: BigInt.BigInteger,
|
||||
) {
|
||||
const serverNonce = toSignedLittleBuffer(serverNonceBigInt, 16);
|
||||
const newNonce = toSignedLittleBuffer(newNonceBigInt, 32);
|
||||
const [hash1, hash2, hash3] = await Promise.all([
|
||||
sha1(Buffer.concat([newNonce, serverNonce])),
|
||||
sha1(Buffer.concat([serverNonce, newNonce])),
|
||||
sha1(Buffer.concat([newNonce, newNonce])),
|
||||
]);
|
||||
const keyBuffer = Buffer.concat([hash1, hash2.slice(0, 12)]);
|
||||
const ivBuffer = Buffer.concat([hash2.slice(12, 20), hash3, newNonce.slice(0, 4)]);
|
||||
return {
|
||||
key: keyBuffer,
|
||||
iv: ivBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
export function convertToLittle(buf: Uint32Array) {
|
||||
const correct = Buffer.alloc(buf.length * 4);
|
||||
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
correct.writeUInt32BE(buf[i], i * 4);
|
||||
}
|
||||
return correct;
|
||||
}
|
||||
|
||||
export function sha1(data: Buffer): Promise<Buffer> {
|
||||
const shaSum = createHash('sha1');
|
||||
shaSum.update(data);
|
||||
return shaSum.digest();
|
||||
}
|
||||
|
||||
export function sha256(data: Buffer): Promise<Buffer> {
|
||||
const shaSum = createHash('sha256');
|
||||
shaSum.update(data);
|
||||
return shaSum.digest();
|
||||
}
|
||||
|
||||
export function modExp(
|
||||
a: bigInt.BigInteger,
|
||||
b: bigInt.BigInteger,
|
||||
n: bigInt.BigInteger,
|
||||
) {
|
||||
a = a.remainder(n);
|
||||
let result = BigInt.one;
|
||||
let x = a;
|
||||
while (b.greater(BigInt.zero)) {
|
||||
const leastSignificantBit = b.remainder(BigInt(2));
|
||||
b = b.divide(BigInt(2));
|
||||
if (leastSignificantBit.eq(BigInt.one)) {
|
||||
result = result.multiply(x);
|
||||
result = result.remainder(n);
|
||||
}
|
||||
x = x.multiply(x);
|
||||
x = x.remainder(n);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getByteArray(integer: BigInt.BigInteger, signed = false) {
|
||||
const bits = integer.toString(2).length;
|
||||
const byteLength = Math.floor((bits + 8 - 1) / 8);
|
||||
return readBufferFromBigInt(BigInt(integer), byteLength, false, signed);
|
||||
}
|
||||
|
||||
export function getRandomInt(min: number, max: number) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function bufferXor(a: Buffer, b: Buffer) {
|
||||
const res = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
res.push(a[i] ^ b[i]);
|
||||
}
|
||||
return Buffer.from(res);
|
||||
}
|
||||
|
||||
// Taken from https://stackoverflow.com/questions/18638900/javascript-crc32/18639999#18639999
|
||||
export const CRC32_TABLE = (() => {
|
||||
let c;
|
||||
const crcTable = [];
|
||||
for (let n = 0; n < 256; n++) {
|
||||
c = n;
|
||||
for (let k = 0; k < 8; k++) {
|
||||
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
|
||||
}
|
||||
crcTable[n] = c;
|
||||
}
|
||||
return crcTable;
|
||||
})();
|
||||
|
||||
export function crc32(buf: Buffer | string) {
|
||||
if (!Buffer.isBuffer(buf)) {
|
||||
buf = Buffer.from(buf);
|
||||
}
|
||||
let crc = -1;
|
||||
|
||||
for (let index = 0; index < buf.length; index++) {
|
||||
const byte = buf[index];
|
||||
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
||||
}
|
||||
return (crc ^ (-1)) >>> 0;
|
||||
}
|
||||
@ -1,14 +1,16 @@
|
||||
const BigInt = require('big-integer');
|
||||
const { constructors } = require('./tl');
|
||||
const {
|
||||
import BigInt from 'big-integer';
|
||||
|
||||
import { pbkdf2 } from './crypto/crypto';
|
||||
import Api from './tl/api';
|
||||
|
||||
import {
|
||||
bigIntMod,
|
||||
generateRandomBytes,
|
||||
modExp,
|
||||
readBigIntFromBuffer,
|
||||
readBufferFromBigInt,
|
||||
sha256,
|
||||
bigIntMod,
|
||||
modExp,
|
||||
generateRandomBytes,
|
||||
} = require('./Helpers');
|
||||
const crypto = require('./crypto/crypto');
|
||||
} from './Helpers';
|
||||
|
||||
const SIZE_FOR_HASH = 256;
|
||||
|
||||
@ -64,12 +66,8 @@ function checkPrimeAndGoodCheck(prime, g) {
|
||||
}
|
||||
}
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* @param primeBytes{Buffer}
|
||||
* @param g{number}
|
||||
*/
|
||||
function checkPrimeAndGood(primeBytes, g) {
|
||||
|
||||
function checkPrimeAndGood(primeBytes: Buffer, g: number) {
|
||||
const goodPrime = Buffer.from([
|
||||
0xC7, 0x1C, 0xAE, 0xB9, 0xC6, 0xB1, 0xC9, 0x04, 0x8E, 0x6C, 0x52, 0x2F, 0x70, 0xF1, 0x3F, 0x73,
|
||||
0x98, 0x0D, 0x40, 0x23, 0x8E, 0x3E, 0x21, 0xC1, 0x49, 0x34, 0xD0, 0x37, 0x56, 0x3D, 0x93, 0x0F,
|
||||
@ -97,53 +95,34 @@ function checkPrimeAndGood(primeBytes, g) {
|
||||
// checkPrimeAndGoodCheck(readBigIntFromBuffer(primeBytes, false), g)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param number{BigInteger}
|
||||
* @param p{BigInteger}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isGoodLarge(number, p) {
|
||||
function isGoodLarge(number: BigInt.BigInteger, p: BigInt.BigInteger): boolean {
|
||||
return (number.greater(BigInt(0)) && (p.subtract(number)
|
||||
.greater(BigInt(0))));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param number {Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function numBytesForHash(number) {
|
||||
function numBytesForHash(number: Buffer): Buffer {
|
||||
return Buffer.concat([Buffer.alloc(SIZE_FOR_HASH - number.length), number]);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param g {Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function bigNumForHash(g) {
|
||||
function bigNumForHash(g: BigInt.BigInteger) {
|
||||
return readBufferFromBigInt(g, SIZE_FOR_HASH, false);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param modexp {BigInteger}
|
||||
* @param prime {BigInteger}
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function isGoodModExpFirst(modexp, prime) {
|
||||
function isGoodModExpFirst(modexp: BigInt.BigInteger, prime: BigInt.BigInteger): boolean {
|
||||
const diff = prime.subtract(modexp);
|
||||
|
||||
const minDiffBitsCount = 2048 - 64;
|
||||
const maxModExpSize = 256;
|
||||
|
||||
return !(diff.lesser(BigInt(0)) || diff.bitLength() < minDiffBitsCount
|
||||
|| modexp.bitLength() < minDiffBitsCount
|
||||
|| Math.floor((modexp.bitLength() + 7) / 8) > maxModExpSize);
|
||||
return !(
|
||||
diff.lesser(BigInt(0))
|
||||
|| diff.bitLength().toJSNumber() < minDiffBitsCount
|
||||
|| modexp.bitLength().toJSNumber() < minDiffBitsCount
|
||||
|| Math.floor((modexp.bitLength().toJSNumber() + 7) / 8) > maxModExpSize
|
||||
);
|
||||
}
|
||||
|
||||
function xor(a, b) {
|
||||
function xor(a: Buffer, b: Buffer) {
|
||||
const length = Math.min(a.length, b.length);
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
@ -153,16 +132,8 @@ function xor(a, b) {
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param password{Buffer}
|
||||
* @param salt{Buffer}
|
||||
* @param iterations{number}
|
||||
* @returns {*}
|
||||
*/
|
||||
|
||||
function pbkdf2sha512(password, salt, iterations) {
|
||||
return crypto.pbkdf2(password, salt, iterations, 64, 'sha512');
|
||||
function pbkdf2sha512(password: Buffer, salt: Buffer, iterations: number): any {
|
||||
return pbkdf2(password, salt, iterations);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -171,19 +142,18 @@ function pbkdf2sha512(password, salt, iterations) {
|
||||
* @param password
|
||||
* @returns {Buffer|*}
|
||||
*/
|
||||
async function computeHash(algo, password) {
|
||||
async function computeHash(
|
||||
algo: Api.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: string,
|
||||
) {
|
||||
const hash1 = await sha256(Buffer.concat([algo.salt1, Buffer.from(password, 'utf-8'), algo.salt1]));
|
||||
const hash2 = await sha256(Buffer.concat([algo.salt2, hash1, algo.salt2]));
|
||||
const hash3 = await pbkdf2sha512(hash2, algo.salt1, 100000);
|
||||
return sha256(Buffer.concat([algo.salt2, hash3, algo.salt2]));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param algo {constructors.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow}
|
||||
* @param password
|
||||
*/
|
||||
async function computeDigest(algo, password) {
|
||||
export async function computeDigest(
|
||||
algo: Api.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow, password: string,
|
||||
) {
|
||||
try {
|
||||
checkPrimeAndGood(algo.p, algo.g);
|
||||
} catch (e) {
|
||||
@ -201,16 +171,21 @@ async function computeDigest(algo, password) {
|
||||
* @param request {constructors.account.Password}
|
||||
* @param password {string}
|
||||
*/
|
||||
async function computeCheck(request, password) {
|
||||
export async function computeCheck(request: Api.account.Password, password: string) {
|
||||
const algo = request.currentAlgo;
|
||||
if (!(algo instanceof constructors.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow)) {
|
||||
throw new Error(`Unsupported password algorithm ${algo.className}`);
|
||||
if (!(algo instanceof Api.PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow)) {
|
||||
throw new Error(`Unsupported password algorithm ${algo?.className}`);
|
||||
}
|
||||
|
||||
const srpB = request.srp_B;
|
||||
const srpId = request.srpId;
|
||||
if (!srpB || !srpId) {
|
||||
throw new Error(`Undefined srp_b ${request}`);
|
||||
}
|
||||
const pwHash = await computeHash(algo, password);
|
||||
const p = readBigIntFromBuffer(algo.p, false);
|
||||
const { g } = algo;
|
||||
const B = readBigIntFromBuffer(request.srp_B, false);
|
||||
const B = readBigIntFromBuffer(srpB, false);
|
||||
try {
|
||||
checkPrimeAndGood(algo.p, g);
|
||||
} catch (e) {
|
||||
@ -221,8 +196,8 @@ async function computeCheck(request, password) {
|
||||
}
|
||||
const x = readBigIntFromBuffer(pwHash, false);
|
||||
const pForHash = numBytesForHash(algo.p);
|
||||
const gForHash = bigNumForHash(g);
|
||||
const bForHash = numBytesForHash(request.srp_B);
|
||||
const gForHash = bigNumForHash(BigInt(g));
|
||||
const bForHash = numBytesForHash(srpB);
|
||||
const gX = modExp(BigInt(g), x, p);
|
||||
const k = readBigIntFromBuffer(await sha256(Buffer.concat([pForHash, gForHash])), false);
|
||||
const kgX = bigIntMod(k.multiply(gX), p);
|
||||
@ -237,12 +212,12 @@ async function computeCheck(request, password) {
|
||||
const aForHash = bigNumForHash(A);
|
||||
const u = readBigIntFromBuffer(await sha256(Buffer.concat([aForHash, bForHash])), false);
|
||||
if (u.greater(BigInt(0))) {
|
||||
return [a, aForHash, u];
|
||||
return { a, aForHash, u };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const [a, aForHash, u] = await generateAndCheckRandom();
|
||||
const { a, aForHash, u } = await generateAndCheckRandom();
|
||||
const gB = bigIntMod(B.subtract(kgX), p);
|
||||
if (!isGoodModExpFirst(gB, p)) {
|
||||
throw new Error('bad gB');
|
||||
@ -267,15 +242,10 @@ async function computeCheck(request, password) {
|
||||
K,
|
||||
]));
|
||||
|
||||
return new constructors.InputCheckPasswordSRP({
|
||||
srpId: request.srpId,
|
||||
return new Api.InputCheckPasswordSRP({
|
||||
srpId,
|
||||
A: Buffer.from(aForHash),
|
||||
M1,
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
computeCheck,
|
||||
computeDigest,
|
||||
};
|
||||
@ -1,717 +0,0 @@
|
||||
const { constructors } = require('./tl');
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const JPEG_HEADER = Buffer.from('ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e19282321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc00011080000000003012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00', 'hex');
|
||||
const JPEG_FOOTER = Buffer.from('ffd9', 'hex');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
function _raiseCastFail(entity, target) {
|
||||
throw new Error(`Cannot cast ${entity.className} to any kind of ${target}`);
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the input peer for the given "entity" (user, chat or channel).
|
||||
|
||||
A ``TypeError`` is raised if the given entity isn't a supported type
|
||||
or if ``check_hash is True`` but the entity's ``accessHash is None``
|
||||
*or* the entity contains ``min`` information. In this case, the hash
|
||||
cannot be used for general purposes, and thus is not returned to avoid
|
||||
any issues which can derive from invalid access hashes.
|
||||
|
||||
Note that ``check_hash`` **is ignored** if an input peer is already
|
||||
passed since in that case we assume the user knows what they're doing.
|
||||
This is key to getting entities by explicitly passing ``hash = 0``.
|
||||
|
||||
* @param entity
|
||||
* @param allowSelf
|
||||
* @param checkHash
|
||||
*/
|
||||
function getInputPeer(entity, allowSelf = true, checkHash = true) {
|
||||
if (entity.SUBCLASS_OF_ID === undefined) {
|
||||
// e.g. custom.Dialog (can't cyclic import).
|
||||
|
||||
if (allowSelf && 'inputEntity' in entity) {
|
||||
return entity.inputEntity;
|
||||
} else if ('entity' in entity) {
|
||||
return getInputPeer(entity.entity);
|
||||
} else {
|
||||
_raiseCastFail(entity, 'InputPeer');
|
||||
}
|
||||
}
|
||||
if (entity.SUBCLASS_OF_ID === 0xc91c90b6) { // crc32(b'InputPeer')
|
||||
return entity;
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.User) {
|
||||
if (entity.isSelf && allowSelf) {
|
||||
return new constructors.InputPeerSelf();
|
||||
} else if (entity.accessHash !== undefined || !checkHash) {
|
||||
return new constructors.InputPeerUser({
|
||||
userId: entity.id,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
} else {
|
||||
throw new Error('User without accessHash or min info cannot be input');
|
||||
}
|
||||
}
|
||||
if (entity instanceof constructors.Chat || entity instanceof constructors.ChatEmpty
|
||||
|| entity instanceof constructors.ChatForbidden) {
|
||||
return new constructors.InputPeerChat({ chatId: entity.id });
|
||||
}
|
||||
if (entity instanceof constructors.Channel) {
|
||||
if (entity.accessHash !== undefined || !checkHash) {
|
||||
return new constructors.InputPeerChannel({
|
||||
channelId: entity.id,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
} else {
|
||||
throw new TypeError('Channel without accessHash or min info cannot be input');
|
||||
}
|
||||
}
|
||||
if (entity instanceof constructors.ChannelForbidden) {
|
||||
// "channelForbidden are never min", and since their hash is
|
||||
// also not optional, we assume that this truly is the case.
|
||||
return new constructors.InputPeerChannel({
|
||||
channelId: entity.id,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.InputUser) {
|
||||
return new constructors.InputPeerUser({
|
||||
userId: entity.userId,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
}
|
||||
if (entity instanceof constructors.InputChannel) {
|
||||
return new constructors.InputPeerChannel({
|
||||
channelId: entity.channelId,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
}
|
||||
if (entity instanceof constructors.UserEmpty) {
|
||||
return new constructors.InputPeerEmpty();
|
||||
}
|
||||
if (entity instanceof constructors.UserFull) {
|
||||
return getInputPeer(entity.user);
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.ChatFull) {
|
||||
return new constructors.InputPeerChat({ chatId: entity.id });
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.PeerChat) {
|
||||
return new constructors.InputPeerChat(entity.chatId);
|
||||
}
|
||||
|
||||
_raiseCastFail(entity, 'InputPeer');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
Similar to :meth:`get_input_peer`, but for :tl:`InputChannel`'s alone.
|
||||
|
||||
.. important::
|
||||
|
||||
This method does not validate for invalid general-purpose access
|
||||
hashes, unlike `get_input_peer`. Consider using instead:
|
||||
``get_input_channel(get_input_peer(channel))``.
|
||||
|
||||
* @param entity
|
||||
* @returns {InputChannel|*}
|
||||
*/
|
||||
/* CONTEST
|
||||
function getInputChannel(entity) {
|
||||
if (entity.SUBCLASS_OF_ID === undefined) {
|
||||
_raiseCastFail(entity, 'InputChannel')
|
||||
}
|
||||
|
||||
if (entity.SUBCLASS_OF_ID === 0x40f202fd) { // crc32(b'InputChannel')
|
||||
return entity
|
||||
}
|
||||
if (entity instanceof constructors.Channel || entity instanceof constructors.ChannelForbidden) {
|
||||
return new constructors.InputChannel({
|
||||
channelId: entity.id,
|
||||
accessHash: entity.accessHash || 0
|
||||
})
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.InputPeerChannel) {
|
||||
return new constructors.InputChannel({
|
||||
channelId: entity.channelId,
|
||||
accessHash: entity.accessHash
|
||||
})
|
||||
}
|
||||
_raiseCastFail(entity, 'InputChannel')
|
||||
}
|
||||
*/
|
||||
/**
|
||||
Similar to :meth:`get_input_peer`, but for :tl:`InputUser`'s alone.
|
||||
|
||||
.. important::
|
||||
|
||||
This method does not validate for invalid general-purpose access
|
||||
hashes, unlike `get_input_peer`. Consider using instead:
|
||||
``get_input_channel(get_input_peer(channel))``.
|
||||
|
||||
* @param entity
|
||||
*/
|
||||
/* CONTEST
|
||||
function getInputUser(entity) {
|
||||
if (entity.SUBCLASS_OF_ID === undefined) {
|
||||
_raiseCastFail(entity, 'InputUser')
|
||||
}
|
||||
if (entity.SUBCLASS_OF_ID === 0xe669bf46) { // crc32(b'InputUser')
|
||||
return entity
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.User) {
|
||||
if (entity.isSelf) {
|
||||
return new constructors.InputPeerSelf()
|
||||
} else {
|
||||
return new constructors.InputUser({
|
||||
userId: entity.id,
|
||||
accessHash: entity.accessHash || 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (entity instanceof constructors.InputPeerSelf) {
|
||||
return new constructors.InputPeerSelf()
|
||||
}
|
||||
if (entity instanceof constructors.UserEmpty || entity instanceof constructors.InputPeerEmpty) {
|
||||
return new constructors.InputUserEmpty()
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.UserFull) {
|
||||
return getInputUser(entity.user)
|
||||
}
|
||||
|
||||
if (entity instanceof constructors.InputPeerUser) {
|
||||
return new constructors.InputUser({
|
||||
userId: entity.userId,
|
||||
accessHash: entity.accessHash
|
||||
})
|
||||
}
|
||||
|
||||
_raiseCastFail(entity, 'InputUser')
|
||||
}
|
||||
*/
|
||||
/**
|
||||
Similar to :meth:`get_input_peer`, but for dialogs
|
||||
* @param dialog
|
||||
*/
|
||||
/* CONTEST
|
||||
function getInputDialog(dialog) {
|
||||
try {
|
||||
if (dialog.SUBCLASS_OF_ID === 0xa21c9795) { // crc32(b'InputDialogPeer')
|
||||
return dialog
|
||||
}
|
||||
if (dialog.SUBCLASS_OF_ID === 0xc91c90b6) { // crc32(b'InputPeer')
|
||||
return new constructors.InputDialogPeer({ peer: dialog })
|
||||
}
|
||||
} catch (e) {
|
||||
_raiseCastFail(dialog, 'InputDialogPeer')
|
||||
}
|
||||
|
||||
try {
|
||||
return new constructors.InputDialogPeer(getInputPeer(dialog))
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
_raiseCastFail(dialog, 'InputDialogPeer')
|
||||
}
|
||||
*/
|
||||
|
||||
/* CONTEST
|
||||
|
||||
function getInputMessage(message) {
|
||||
try {
|
||||
if (typeof message == 'number') { // This case is really common too
|
||||
return new constructors.InputMessageID({
|
||||
id: message,
|
||||
})
|
||||
} else if (message.SUBCLASS_OF_ID === 0x54b6bcc5) { // crc32(b'InputMessage')
|
||||
return message
|
||||
} else if (message.SUBCLASS_OF_ID === 0x790009e3) { // crc32(b'Message')
|
||||
return new constructors.InputMessageID(message.id)
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
_raiseCastFail(message, 'InputMessage')
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds the JPG header and footer to a stripped image.
|
||||
* Ported from https://github.com/telegramdesktop/
|
||||
* tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225
|
||||
|
||||
* @param stripped{Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function strippedPhotoToJpg(stripped) {
|
||||
// Note: Changes here should update _stripped_real_length
|
||||
if (stripped.length < 3 || stripped[0] !== 1) {
|
||||
return stripped;
|
||||
}
|
||||
const header = Buffer.from(JPEG_HEADER);
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
header[164] = stripped[1];
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
header[166] = stripped[2];
|
||||
return Buffer.concat([header, stripped.slice(3), JPEG_FOOTER]);
|
||||
}
|
||||
|
||||
/* CONTEST
|
||||
function getInputLocation(location) {
|
||||
try {
|
||||
if (!location.SUBCLASS_OF_ID) {
|
||||
throw new Error()
|
||||
}
|
||||
if (location.SUBCLASS_OF_ID === 0x1523d462) {
|
||||
return {
|
||||
dcId: null,
|
||||
inputLocation: location
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_raiseCastFail(location, 'InputFileLocation')
|
||||
}
|
||||
if (location instanceof constructors.Message) {
|
||||
location = location.media
|
||||
}
|
||||
|
||||
if (location instanceof constructors.MessageMediaDocument) {
|
||||
location = location.document
|
||||
} else if (location instanceof constructors.MessageMediaPhoto) {
|
||||
location = location.photo
|
||||
}
|
||||
|
||||
if (location instanceof constructors.Document) {
|
||||
return {
|
||||
dcId: location.dcId,
|
||||
inputLocation: new constructors.InputDocumentFileLocation({
|
||||
id: location.id,
|
||||
accessHash: location.accessHash,
|
||||
fileReference: location.fileReference,
|
||||
thumbSize: '', // Presumably to download one of its thumbnails
|
||||
}),
|
||||
}
|
||||
} else if (location instanceof constructors.Photo) {
|
||||
return {
|
||||
dcId: location.dcId,
|
||||
inputLocation: new constructors.InputPhotoFileLocation({
|
||||
id: location.id,
|
||||
accessHash: location.accessHash,
|
||||
fileReference: location.fileReference,
|
||||
thumbSize: location.sizes[location.sizes.length - 1].type,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
if (location instanceof constructors.FileLocationToBeDeprecated) {
|
||||
throw new Error('Unavailable location cannot be used as input')
|
||||
}
|
||||
_raiseCastFail(location, 'InputFileLocation')
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the appropriated part size when downloading files,
|
||||
* given an initial file size.
|
||||
* @param fileSize
|
||||
* @returns {Number}
|
||||
*/
|
||||
function getDownloadPartSize(fileSize) {
|
||||
if (fileSize <= 65536) { // 64KB
|
||||
return 64;
|
||||
}
|
||||
if (fileSize <= 104857600) { // 100MB
|
||||
return 128;
|
||||
}
|
||||
if (fileSize <= 786432000) { // 750MB
|
||||
return 256;
|
||||
}
|
||||
if (fileSize <= 2097152000) { // 2000MB
|
||||
return 512;
|
||||
}
|
||||
if (fileSize <= 4194304000) { // 4000MB
|
||||
return 1024;
|
||||
}
|
||||
|
||||
throw new Error('File size too large');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriated part size when uploading files,
|
||||
* given an initial file size.
|
||||
* @param fileSize
|
||||
* @returns {Number}
|
||||
*/
|
||||
function getUploadPartSize(fileSize) {
|
||||
if (fileSize <= 104857600) { // 100MB
|
||||
return 128;
|
||||
}
|
||||
if (fileSize <= 786432000) { // 750MB
|
||||
return 256;
|
||||
}
|
||||
if (fileSize <= 2097152000) { // 2000MB
|
||||
return 512;
|
||||
}
|
||||
if (fileSize <= 4194304000) { // 4000MB
|
||||
return 512;
|
||||
}
|
||||
|
||||
throw new Error('File size too large');
|
||||
}
|
||||
|
||||
/* CONTEST
|
||||
function getPeer(peer) {
|
||||
try {
|
||||
if (typeof peer === 'number') {
|
||||
const res = resolveId(peer)
|
||||
|
||||
if (res[1] === constructors.PeerChannel) {
|
||||
return new res[1]({ channelId: res[0] })
|
||||
} else if (res[1] === constructors.PeerChat) {
|
||||
return new res[1]({ chatId: res[0] })
|
||||
} else {
|
||||
return new res[1]({ userId: res[0] })
|
||||
}
|
||||
}
|
||||
if (peer.SUBCLASS_OF_ID === undefined) {
|
||||
throw new Error()
|
||||
}
|
||||
if (peer.SUBCLASS_OF_ID === 0x2d45687) {
|
||||
return peer
|
||||
} else if (peer instanceof constructors.contacts.ResolvedPeer ||
|
||||
peer instanceof constructors.InputNotifyPeer || peer instanceof constructors.TopPeer ||
|
||||
peer instanceof constructors.Dialog || peer instanceof constructors.DialogPeer) {
|
||||
return peer.peer
|
||||
} else if (peer instanceof constructors.ChannelFull) {
|
||||
return new constructors.PeerChannel({ channelId: peer.id })
|
||||
}
|
||||
if (peer.SUBCLASS_OF_ID === 0x7d7c6f86 || peer.SUBCLASS_OF_ID === 0xd9c7fc18) {
|
||||
// ChatParticipant, ChannelParticipant
|
||||
return new constructors.PeerUser({ userId: peer.userId })
|
||||
}
|
||||
peer = getInputPeer(peer, false, false)
|
||||
|
||||
if (peer instanceof constructors.InputPeerUser) {
|
||||
return new constructors.PeerUser({ userId: peer.userId })
|
||||
} else if (peer instanceof constructors.InputPeerChat) {
|
||||
return new constructors.PeerChat({ chatId: peer.chatId })
|
||||
} else if (peer instanceof constructors.InputPeerChannel) {
|
||||
return new constructors.PeerChannel({ channelId: peer.channelId })
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
_raiseCastFail(peer, 'peer')
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
Convert the given peer into its marked ID by default.
|
||||
|
||||
This "mark" comes from the "bot api" format, and with it the peer type
|
||||
can be identified back. User ID is left unmodified, chat ID is negated,
|
||||
and channel ID is prefixed with -100:
|
||||
|
||||
* ``userId``
|
||||
* ``-chatId``
|
||||
* ``-100channel_id``
|
||||
|
||||
The original ID and the peer type class can be returned with
|
||||
a call to :meth:`resolve_id(marked_id)`.
|
||||
* @param peer
|
||||
* @param addMark
|
||||
*/
|
||||
/* CONTEST
|
||||
function getPeerId(peer, addMark = true) {
|
||||
// First we assert it's a Peer TLObject, or early return for integers
|
||||
if (typeof peer == 'number') {
|
||||
return addMark ? peer : resolveId(peer)[0]
|
||||
}
|
||||
|
||||
// Tell the user to use their client to resolve InputPeerSelf if we got one
|
||||
if (peer instanceof constructors.InputPeerSelf) {
|
||||
_raiseCastFail(peer, 'int (you might want to use client.get_peer_id)')
|
||||
}
|
||||
|
||||
try {
|
||||
peer = getPeer(peer)
|
||||
} catch (e) {
|
||||
_raiseCastFail(peer, 'int')
|
||||
}
|
||||
if (peer instanceof constructors.PeerUser) {
|
||||
return peer.userId
|
||||
} else if (peer instanceof constructors.PeerChat) {
|
||||
// Check in case the user mixed things up to avoid blowing up
|
||||
if (!(0 < peer.chatId <= 0x7fffffff)) {
|
||||
peer.chatId = resolveId(peer.chatId)[0]
|
||||
}
|
||||
|
||||
return addMark ? -(peer.chatId) : peer.chatId
|
||||
} else { // if (peer instanceof constructors.PeerChannel)
|
||||
// Check in case the user mixed things up to avoid blowing up
|
||||
if (!(0 < peer.channelId <= 0x7fffffff)) {
|
||||
peer.channelId = resolveId(peer.channelId)[0]
|
||||
}
|
||||
if (!addMark) {
|
||||
return peer.channelId
|
||||
}
|
||||
// Concat -100 through math tricks, .to_supergroup() on
|
||||
// Madeline IDs will be strictly positive -> log works.
|
||||
try {
|
||||
return -(peer.channelId + Math.pow(10, Math.floor(Math.log10(peer.channelId) + 3)))
|
||||
} catch (e) {
|
||||
throw new Error('Cannot get marked ID of a channel unless its ID is strictly positive')
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Given a marked ID, returns the original ID and its :tl:`Peer` type.
|
||||
* @param markedId
|
||||
*/
|
||||
/* CONTEST
|
||||
function resolveId(markedId) {
|
||||
if (markedId >= 0) {
|
||||
return [markedId, constructors.PeerUser]
|
||||
}
|
||||
|
||||
// There have been report of chat IDs being 10000xyz, which means their
|
||||
// marked version is -10000xyz, which in turn looks like a channel but
|
||||
// it becomes 00xyz (= xyz). Hence, we must assert that there are only
|
||||
// two zeroes.
|
||||
const m = markedId.toString()
|
||||
.match(/-100([^0]\d*)/)
|
||||
if (m) {
|
||||
return [parseInt(m[1]), constructors.PeerChannel]
|
||||
}
|
||||
return [-markedId, constructors.PeerChat]
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* returns an entity pair
|
||||
* @param entityId
|
||||
* @param entities
|
||||
* @param cache
|
||||
* @param getInputPeer
|
||||
* @returns {{inputEntity: *, entity: *}}
|
||||
* @private
|
||||
*/
|
||||
|
||||
/* CONTEST
|
||||
|
||||
function _getEntityPair(entityId, entities, cache, getInputPeer = getInputPeer) {
|
||||
const entity = entities.get(entityId)
|
||||
let inputEntity = cache[entityId]
|
||||
if (inputEntity === undefined) {
|
||||
try {
|
||||
inputEntity = getInputPeer(inputEntity)
|
||||
} catch (e) {
|
||||
inputEntity = null
|
||||
}
|
||||
}
|
||||
return {
|
||||
entity,
|
||||
inputEntity
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function getMessageId(message) {
|
||||
if (message === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof message === 'number') {
|
||||
return message;
|
||||
}
|
||||
if (message.SUBCLASS_OF_ID === 0x790009e3) { // crc32(b'Message')
|
||||
return message.id;
|
||||
}
|
||||
throw new Error(`Invalid message type: ${message.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
Parses the given username or channel access hash, given
|
||||
a string, username or URL. Returns a tuple consisting of
|
||||
both the stripped, lowercase username and whether it is
|
||||
a joinchat/ hash (in which case is not lowercase'd).
|
||||
|
||||
Returns ``(None, False)`` if the ``username`` or link is not valid.
|
||||
|
||||
* @param username {string}
|
||||
*/
|
||||
|
||||
/* CONTEST
|
||||
|
||||
function parseUsername(username) {
|
||||
username = username.trim()
|
||||
const m = username.match(USERNAME_RE) || username.match(TG_JOIN_RE)
|
||||
if (m) {
|
||||
username = username.replace(m[0], '')
|
||||
if (m[1]) {
|
||||
return {
|
||||
username: username,
|
||||
isInvite: true
|
||||
}
|
||||
} else {
|
||||
username = rtrim(username, '/')
|
||||
}
|
||||
}
|
||||
if (username.match(VALID_USERNAME_RE)) {
|
||||
return {
|
||||
username: username.toLowerCase(),
|
||||
isInvite: false
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
username: null,
|
||||
isInvite: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rtrim(s, mask) {
|
||||
while (~mask.indexOf(s[s.length - 1])) {
|
||||
s = s.slice(0, -1)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Gets the display name for the given :tl:`User`,
|
||||
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise
|
||||
* @param entity
|
||||
*/
|
||||
function getDisplayName(entity) {
|
||||
if (entity instanceof constructors.User) {
|
||||
if (entity.lastName && entity.firstName) {
|
||||
return `${entity.firstName} ${entity.lastName}`;
|
||||
} else if (entity.firstName) {
|
||||
return entity.firstName;
|
||||
} else if (entity.lastName) {
|
||||
return entity.lastName;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else if (entity instanceof constructors.Chat || entity instanceof constructors.Channel) {
|
||||
return entity.title;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* check if a given item is an array like or not
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
*/
|
||||
|
||||
/* CONTEST
|
||||
Duplicate ?
|
||||
function isListLike(item) {
|
||||
return (
|
||||
Array.isArray(item) ||
|
||||
(Boolean(item) &&
|
||||
typeof item === 'object' &&
|
||||
typeof (item.length) === 'number' &&
|
||||
(item.length === 0 ||
|
||||
(item.length > 0 &&
|
||||
(item.length - 1) in item)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Returns the appropriate DC based on the id
|
||||
* @param dcId the id of the DC.
|
||||
* @param downloadDC whether to use -1 DCs or not
|
||||
* (These only support downloading/uploading and not creating a new AUTH key)
|
||||
* @return {{port: number, ipAddress: string, id: number}}
|
||||
*/
|
||||
function getDC(dcId, downloadDC = false) {
|
||||
// TODO Move to external config
|
||||
switch (dcId) {
|
||||
case 1:
|
||||
return {
|
||||
id: 1,
|
||||
ipAddress: `zws1${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
id: 2,
|
||||
ipAddress: `zws2${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
id: 3,
|
||||
ipAddress: `zws3${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
id: 4,
|
||||
ipAddress: `zws4${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
id: 5,
|
||||
ipAddress: `zws5${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Cannot find the DC with the ID of ${dcId}`);
|
||||
}
|
||||
// TODO chose based on current connection method
|
||||
/*
|
||||
if (!this._config) {
|
||||
this._config = await this.invoke(new requests.help.GetConfig())
|
||||
}
|
||||
if (cdn && !this._cdnConfig) {
|
||||
this._cdnConfig = await this.invoke(new requests.help.GetCdnConfig())
|
||||
for (const pk of this._cdnConfig.publicKeys) {
|
||||
addKey(pk.publicKey)
|
||||
}
|
||||
}
|
||||
for (const DC of this._config.dcOptions) {
|
||||
if (DC.id === dcId && Boolean(DC.ipv6) === this._useIPV6 && Boolean(DC.cdn) === cdn) {
|
||||
return DC
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMessageId,
|
||||
// _getEntityPair,
|
||||
// getInputMessage,
|
||||
// getInputDialog,
|
||||
// getInputUser,
|
||||
// getInputChannel,
|
||||
getInputPeer,
|
||||
// parsePhone,
|
||||
// parseUsername,
|
||||
// getPeer,
|
||||
// getPeerId,
|
||||
getDisplayName,
|
||||
// resolveId,
|
||||
// isListLike,
|
||||
getDownloadPartSize,
|
||||
getUploadPartSize,
|
||||
// getInputLocation,
|
||||
strippedPhotoToJpg,
|
||||
getDC,
|
||||
};
|
||||
253
src/lib/gramjs/Utils.ts
Normal file
253
src/lib/gramjs/Utils.ts
Normal file
@ -0,0 +1,253 @@
|
||||
import type { Entity, EntityLike } from './types';
|
||||
|
||||
import { Api } from './tl';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const JPEG_HEADER = Buffer.from('ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e19282321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc00011080000000003012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00', 'hex');
|
||||
const JPEG_FOOTER = Buffer.from('ffd9', 'hex');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
function _raiseCastFail(entity: any, target: string) {
|
||||
throw new Error(`Cannot cast ${entity.className} to any kind of ${target}`);
|
||||
}
|
||||
|
||||
/**
|
||||
Gets the input peer for the given "entity" (user, chat or channel).
|
||||
|
||||
A ``TypeError`` is raised if the given entity isn't a supported type
|
||||
or if ``check_hash is True`` but the entity's ``accessHash is None``
|
||||
*or* the entity contains ``min`` information. In this case, the hash
|
||||
cannot be used for general purposes, and thus is not returned to avoid
|
||||
any issues which can derive from invalid access hashes.
|
||||
|
||||
Note that ``check_hash`` **is ignored** if an input peer is already
|
||||
passed since in that case we assume the user knows what they're doing.
|
||||
This is key to getting entities by explicitly passing ``hash = 0``.
|
||||
|
||||
* @param entity
|
||||
* @param allowSelf
|
||||
* @param checkHash
|
||||
*/
|
||||
export function getInputPeer(entity: Entity, allowSelf = true, checkHash = true): Api.TypeInputPeer {
|
||||
if (entity.SUBCLASS_OF_ID === 0xc91c90b6) { // crc32(b'InputPeer')
|
||||
return entity;
|
||||
}
|
||||
|
||||
if (entity instanceof Api.User) {
|
||||
if (entity.self && allowSelf) {
|
||||
return new Api.InputPeerSelf();
|
||||
} else if (entity.accessHash !== undefined || !checkHash) {
|
||||
return new Api.InputPeerUser({
|
||||
userId: entity.id,
|
||||
accessHash: entity.accessHash!,
|
||||
});
|
||||
} else {
|
||||
throw new Error('User without accessHash or min info cannot be input');
|
||||
}
|
||||
}
|
||||
if (entity instanceof Api.Chat || entity instanceof Api.ChatEmpty
|
||||
|| entity instanceof Api.ChatForbidden) {
|
||||
return new Api.InputPeerChat({ chatId: entity.id });
|
||||
}
|
||||
if (entity instanceof Api.Channel) {
|
||||
if (entity.accessHash !== undefined || !checkHash) {
|
||||
return new Api.InputPeerChannel({
|
||||
channelId: entity.id,
|
||||
accessHash: entity.accessHash!,
|
||||
});
|
||||
} else {
|
||||
throw new TypeError('Channel without accessHash or min info cannot be input');
|
||||
}
|
||||
}
|
||||
if (entity instanceof Api.ChannelForbidden) {
|
||||
// "channelForbidden are never min", and since their hash is
|
||||
// also not optional, we assume that this truly is the case.
|
||||
return new Api.InputPeerChannel({
|
||||
channelId: entity.id,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
}
|
||||
|
||||
if (entity instanceof Api.InputUser) {
|
||||
return new Api.InputPeerUser({
|
||||
userId: entity.userId,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
}
|
||||
if (entity instanceof Api.InputChannel) {
|
||||
return new Api.InputPeerChannel({
|
||||
channelId: entity.channelId,
|
||||
accessHash: entity.accessHash,
|
||||
});
|
||||
}
|
||||
if (entity instanceof Api.UserEmpty) {
|
||||
return new Api.InputPeerEmpty();
|
||||
}
|
||||
|
||||
_raiseCastFail(entity, 'InputPeer');
|
||||
return new Api.InputPeerEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the JPG header and footer to a stripped image.
|
||||
* Ported from https://github.com/telegramdesktop/
|
||||
* tdesktop/blob/bec39d89e19670eb436dc794a8f20b657cb87c71/Telegram/SourceFiles/ui/image/image.cpp#L225
|
||||
|
||||
* @param stripped{Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
export function strippedPhotoToJpg(stripped: Buffer) {
|
||||
// Note: Changes here should update _stripped_real_length
|
||||
if (stripped.length < 3 || stripped[0] !== 1) {
|
||||
return stripped;
|
||||
}
|
||||
const header = Buffer.from(JPEG_HEADER);
|
||||
header[164] = stripped[1];
|
||||
header[166] = stripped[2];
|
||||
return Buffer.concat([header, stripped.slice(3), JPEG_FOOTER]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriated part size when downloading files,
|
||||
* given an initial file size.
|
||||
* @param fileSize
|
||||
* @returns {Number}
|
||||
*/
|
||||
export function getDownloadPartSize(fileSize: number) {
|
||||
if (fileSize <= 65536) { // 64KB
|
||||
return 64;
|
||||
}
|
||||
if (fileSize <= 104857600) { // 100MB
|
||||
return 128;
|
||||
}
|
||||
if (fileSize <= 786432000) { // 750MB
|
||||
return 256;
|
||||
}
|
||||
if (fileSize <= 2097152000) { // 2000MB
|
||||
return 512;
|
||||
}
|
||||
if (fileSize <= 4194304000) { // 4000MB
|
||||
return 1024;
|
||||
}
|
||||
|
||||
throw new Error('File size too large');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriated part size when uploading files,
|
||||
* given an initial file size.
|
||||
* @param fileSize
|
||||
* @returns {Number}
|
||||
*/
|
||||
export function getUploadPartSize(fileSize: number) {
|
||||
if (fileSize <= 104857600) { // 100MB
|
||||
return 128;
|
||||
}
|
||||
if (fileSize <= 786432000) { // 750MB
|
||||
return 256;
|
||||
}
|
||||
if (fileSize <= 2097152000) { // 2000MB
|
||||
return 512;
|
||||
}
|
||||
if (fileSize <= 4194304000) { // 4000MB
|
||||
return 512;
|
||||
}
|
||||
|
||||
throw new Error('File size too large');
|
||||
}
|
||||
|
||||
export function getMessageId(message: number | Api.TypeMessage) {
|
||||
if (message === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof message === 'number') {
|
||||
return message;
|
||||
}
|
||||
if (message.SUBCLASS_OF_ID === 0x790009e3) { // crc32(b'Message')
|
||||
return message.id;
|
||||
}
|
||||
throw new Error(`Invalid message type: ${message.constructor.name}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name for the given :tl:`User`,
|
||||
:tl:`Chat` or :tl:`Channel`. Returns an empty string otherwise
|
||||
* @param entity
|
||||
*/
|
||||
export function getDisplayName(entity: Entity) {
|
||||
if (entity instanceof Api.User) {
|
||||
if (entity.lastName && entity.firstName) {
|
||||
return `${entity.firstName} ${entity.lastName}`;
|
||||
} else if (entity.firstName) {
|
||||
return entity.firstName;
|
||||
} else if (entity.lastName) {
|
||||
return entity.lastName;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else if (entity instanceof Api.Chat || entity instanceof Api.Channel) {
|
||||
return entity.title;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate DC based on the id
|
||||
* @param dcId the id of the DC.
|
||||
* @param downloadDC whether to use -1 DCs or not
|
||||
* (These only support downloading/uploading and not creating a new AUTH key)
|
||||
* @return {{port: number, ipAddress: string, id: number}}
|
||||
*/
|
||||
export function getDC(dcId: number, downloadDC = false) {
|
||||
// TODO Move to external config
|
||||
switch (dcId) {
|
||||
case 1:
|
||||
return {
|
||||
id: 1,
|
||||
ipAddress: `zws1${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
id: 2,
|
||||
ipAddress: `zws2${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 3:
|
||||
return {
|
||||
id: 3,
|
||||
ipAddress: `zws3${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
id: 4,
|
||||
ipAddress: `zws4${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
id: 5,
|
||||
ipAddress: `zws5${downloadDC ? '-1' : ''}.web.telegram.org`,
|
||||
port: 443,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Cannot find the DC with the ID of ${dcId}`);
|
||||
}
|
||||
// TODO chose based on current connection method
|
||||
/*
|
||||
if (!this._config) {
|
||||
this._config = await this.invoke(new requests.help.GetConfig())
|
||||
}
|
||||
if (cdn && !this._cdnConfig) {
|
||||
this._cdnConfig = await this.invoke(new requests.help.GetCdnConfig())
|
||||
for (const pk of this._cdnConfig.publicKeys) {
|
||||
addKey(pk.publicKey)
|
||||
}
|
||||
}
|
||||
for (const DC of this._config.dcOptions) {
|
||||
if (DC.id === dcId && Boolean(DC.ipv6) === this._useIPV6 && Boolean(DC.cdn) === cdn) {
|
||||
return DC
|
||||
}
|
||||
} */
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
module.exports = '0.0.2';
|
||||
@ -1,8 +1,7 @@
|
||||
import type TelegramClient from './TelegramClient';
|
||||
|
||||
import errors from '../errors';
|
||||
// eslint-disable-next-line import/no-named-default
|
||||
import { default as Api } from '../tl/api';
|
||||
import { EmailUnconfirmedError, PasswordModifiedError, RPCError } from '../errors';
|
||||
import Api from '../tl/api';
|
||||
|
||||
import { generateRandomBytes } from '../Helpers';
|
||||
import { computeCheck, computeDigest } from '../Password';
|
||||
@ -23,7 +22,7 @@ export interface TwoFaPasswordParams {
|
||||
}
|
||||
|
||||
export type TmpPasswordResult = Api.account.TmpPassword | { error: string } | undefined;
|
||||
export type PasswordResult = Api.account.Password | { error: string } | undefined;
|
||||
export type PasswordResult = Api.TypeInputCheckPasswordSRP | { error: string } | undefined;
|
||||
|
||||
/**
|
||||
* Changes the 2FA settings of the logged in user.
|
||||
@ -83,9 +82,13 @@ export async function updateTwoFaSettings(
|
||||
|
||||
const pwd = await client.invoke(new Api.account.GetPassword());
|
||||
|
||||
if (!(pwd.newAlgo instanceof Api.PasswordKdfAlgoUnknown)) {
|
||||
pwd.newAlgo.salt1 = Buffer.concat([pwd.newAlgo.salt1, generateRandomBytes(32)]);
|
||||
const newAlgo = pwd.newAlgo;
|
||||
|
||||
if (newAlgo instanceof Api.PasswordKdfAlgoUnknown) {
|
||||
throw new Error('Password algorithm is unknown');
|
||||
}
|
||||
|
||||
newAlgo.salt1 = Buffer.concat([newAlgo.salt1, generateRandomBytes(32)]);
|
||||
if (!pwd.hasPassword && currentPassword) {
|
||||
currentPassword = undefined;
|
||||
}
|
||||
@ -101,8 +104,8 @@ export async function updateTwoFaSettings(
|
||||
await client.invoke(new Api.account.UpdatePasswordSettings({
|
||||
password,
|
||||
newSettings: new Api.account.PasswordInputSettings({
|
||||
newAlgo: pwd.newAlgo,
|
||||
newPasswordHash: newPassword ? await computeDigest(pwd.newAlgo, newPassword) : Buffer.alloc(0),
|
||||
newAlgo,
|
||||
newPasswordHash: newPassword ? await computeDigest(newAlgo, newPassword) : Buffer.alloc(0),
|
||||
hint,
|
||||
email,
|
||||
// not explained what it does and it seems to always be set to empty in tdesktop
|
||||
@ -110,7 +113,7 @@ export async function updateTwoFaSettings(
|
||||
}),
|
||||
}));
|
||||
} catch (e) {
|
||||
if (e instanceof errors.EmailUnconfirmedError) {
|
||||
if (e instanceof EmailUnconfirmedError) {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
@ -147,9 +150,9 @@ export async function getTmpPassword(client: TelegramClient, currentPassword: st
|
||||
}));
|
||||
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
if (err.message === 'PASSWORD_HASH_INVALID') {
|
||||
return { error: err.message };
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'PASSWORD_HASH_INVALID') {
|
||||
return { error: err.errorMessage };
|
||||
}
|
||||
|
||||
throw err;
|
||||
@ -162,7 +165,7 @@ export async function getCurrentPassword(
|
||||
currentPassword,
|
||||
onPasswordCodeError,
|
||||
}: TwoFaPasswordParams,
|
||||
) {
|
||||
): Promise<PasswordResult> {
|
||||
const pwd = await client.invoke(new Api.account.GetPassword());
|
||||
|
||||
if (!pwd) {
|
||||
@ -172,10 +175,11 @@ export async function getCurrentPassword(
|
||||
try {
|
||||
return currentPassword ? await computeCheck(pwd, currentPassword!) : new Api.InputCheckPasswordEmpty();
|
||||
} catch (err: any) {
|
||||
if (err instanceof errors.PasswordModifiedError) {
|
||||
return onPasswordCodeError!(err);
|
||||
} else if (err.message === 'PASSWORD_HASH_INVALID') {
|
||||
return { error: err.message };
|
||||
if (err instanceof PasswordModifiedError) {
|
||||
onPasswordCodeError!(err);
|
||||
return undefined;
|
||||
} else if (err instanceof RPCError && err.errorMessage ==='PASSWORD_HASH_INVALID') {
|
||||
return { error: err.errorMessage };
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import BigInt from 'big-integer';
|
||||
|
||||
import type { DownloadFileParams } from './downloadFile';
|
||||
import type { DownloadFileWithDcParams } from './downloadFile';
|
||||
import type { MockTypes } from './mockUtils/MockTypes';
|
||||
import type { SizeType } from './TelegramClient';
|
||||
|
||||
import { GENERAL_TOPIC_ID } from '../../../config';
|
||||
import { UpdateConnectionState } from '../network';
|
||||
@ -22,7 +23,7 @@ import { downloadFile } from './downloadFile';
|
||||
|
||||
import MockSender from './MockSender';
|
||||
|
||||
const sizeTypes = ['u', 'v', 'w', 'y', 'd', 'x', 'c', 'm', 'b', 'a', 's', 'f'];
|
||||
const sizeTypes: SizeType[] = ['u', 'v', 'w', 'y', 'd', 'x', 'c', 'm', 'b', 'a', 's', 'f'];
|
||||
|
||||
class TelegramClient {
|
||||
private invokeMiddleware?: <A, R>(mockClient: TelegramClient, request: Api.Request<A, R>)
|
||||
@ -282,7 +283,7 @@ class TelegramClient {
|
||||
return new MockSender(this);
|
||||
}
|
||||
|
||||
downloadFile(inputLocation: any, args: DownloadFileParams) {
|
||||
downloadFile(inputLocation: any, args: DownloadFileWithDcParams) {
|
||||
return downloadFile(this as any, inputLocation, args);
|
||||
}
|
||||
|
||||
|
||||
41
src/lib/gramjs/client/TelegramClient.d.ts
vendored
41
src/lib/gramjs/client/TelegramClient.d.ts
vendored
@ -1,41 +0,0 @@
|
||||
import type {
|
||||
PasswordResult, TmpPasswordResult, TwoFaParams, TwoFaPasswordParams, updateTwoFaSettings,
|
||||
} from './2fa';
|
||||
import type { BotAuthParams, UserAuthParams } from './auth';
|
||||
import type { downloadFile, DownloadFileParams } from './downloadFile';
|
||||
import type { uploadFile, UploadFileParams } from './uploadFile';
|
||||
|
||||
import type { Api } from '..';
|
||||
|
||||
declare class TelegramClient {
|
||||
constructor(...args: any);
|
||||
|
||||
async start(authParams: UserAuthParams | BotAuthParams);
|
||||
|
||||
async invoke<R extends Api.AnyRequest>(
|
||||
request: R, dcId?: number, abortSignal?: AbortSignal, shouldRetryOnTimeout?: boolean,
|
||||
): Promise<R['__response']>;
|
||||
|
||||
async invokeBeacon<R extends Api.AnyRequest>(request: R, dcId?: number): void;
|
||||
|
||||
async uploadFile(uploadParams: UploadFileParams): ReturnType<typeof uploadFile>;
|
||||
|
||||
async downloadFile(uploadParams: DownloadFileParams): ReturnType<typeof downloadFile>;
|
||||
|
||||
async updateTwoFaSettings(Params: TwoFaParams): ReturnType<typeof updateTwoFaSettings>;
|
||||
|
||||
async getTmpPassword(currentPassword: string, ttl?: number): Promise<TmpPasswordResult>;
|
||||
|
||||
async getCurrentPassword(Params: TwoFaPasswordParams): Promise<PasswordResult>;
|
||||
|
||||
setPingCallback(callback: () => Promise<void>);
|
||||
|
||||
setForceHttpTransport: (forceHttpTransport: boolean) => void;
|
||||
|
||||
setAllowHttpTransport: (allowHttpTransport: boolean) => void;
|
||||
|
||||
// Untyped methods.
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export default TelegramClient;
|
||||
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,9 @@ import Api from '../tl/api';
|
||||
|
||||
import { sleep } from '../Helpers';
|
||||
import { computeCheck as computePasswordSrpCheck } from '../Password';
|
||||
import utils from '../Utils';
|
||||
import { getDisplayName } from '../Utils';
|
||||
import { Update } from './TelegramClient';
|
||||
import { RPCError } from '../errors';
|
||||
|
||||
export interface UserAuthParams {
|
||||
phoneNumber: string | (() => Promise<string>);
|
||||
@ -48,7 +50,7 @@ export async function authFlow(
|
||||
me = await signInUserWithPreferredMethod(client, apiCredentials, authParams);
|
||||
}
|
||||
|
||||
client._log.info('Signed in successfully as', utils.getDisplayName(me));
|
||||
client._log.info(`Signed in successfully as ${getDisplayName(me)}`);
|
||||
}
|
||||
|
||||
export function signInUserWithPreferredMethod(
|
||||
@ -67,8 +69,8 @@ export async function checkAuthorization(client: TelegramClient, shouldThrow = f
|
||||
try {
|
||||
await client.invoke(new Api.updates.GetState());
|
||||
return true;
|
||||
} catch (e: any) {
|
||||
if (e.message === 'Disconnect' || shouldThrow) throw e;
|
||||
} catch (err: unknown) {
|
||||
if ((err instanceof RPCError && err.errorMessage === 'Disconnect') || shouldThrow) throw err;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -89,8 +91,8 @@ async function signInUserWithWebToken(
|
||||
} else {
|
||||
throw new Error('SIGN_UP_REQUIRED');
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'SESSION_PASSWORD_NEEDED') {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'SESSION_PASSWORD_NEEDED') {
|
||||
return signInWithPassword(client, apiCredentials, authParams, true);
|
||||
} else {
|
||||
client._log.error(`Failed to login with web token: ${err}`);
|
||||
@ -116,8 +118,8 @@ async function signInUser(
|
||||
if (typeof authParams.phoneNumber === 'function') {
|
||||
try {
|
||||
phoneNumber = await authParams.phoneNumber();
|
||||
} catch (err: any) {
|
||||
if (err.message === 'RESTART_AUTH_WITH_QR') {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.message === 'RESTART_AUTH_WITH_QR') {
|
||||
return signInUserWithQrCode(client, apiCredentials, authParams);
|
||||
}
|
||||
|
||||
@ -153,9 +155,9 @@ async function signInUser(
|
||||
try {
|
||||
try {
|
||||
phoneCode = await authParams.phoneCode(isCodeViaApp);
|
||||
} catch (err: any) {
|
||||
} catch (err: unknown) {
|
||||
// This is the support for changing phone number from the phone code screen.
|
||||
if (err.message === 'RESTART_AUTH') {
|
||||
if (err instanceof Error && err.message === 'RESTART_AUTH') {
|
||||
return signInUser(client, apiCredentials, authParams);
|
||||
}
|
||||
}
|
||||
@ -179,11 +181,13 @@ async function signInUser(
|
||||
}
|
||||
|
||||
return result.user;
|
||||
} catch (err: any) {
|
||||
if (err.message === 'SESSION_PASSWORD_NEEDED') {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'SESSION_PASSWORD_NEEDED') {
|
||||
return signInWithPassword(client, apiCredentials, authParams);
|
||||
} else {
|
||||
} else if (err instanceof Error) {
|
||||
authParams.onError(err);
|
||||
} else {
|
||||
console.warn('Unexpected error:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -255,15 +259,15 @@ async function signInUserWithQrCode(
|
||||
if (update instanceof Api.UpdateLoginToken) {
|
||||
resolve();
|
||||
}
|
||||
}, { build: (update: object) => update });
|
||||
}, { build: (update: Update) => update });
|
||||
});
|
||||
|
||||
try {
|
||||
// Either we receive an update that QR is successfully scanned,
|
||||
// or we receive a rejection caused by user going back to the regular auth form
|
||||
await Promise.race([updatePromise, inputPromise]);
|
||||
} catch (err: any) {
|
||||
if (err.message === 'RESTART_AUTH') {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error && err.message === 'RESTART_AUTH') {
|
||||
return await signInUser(client, apiCredentials, authParams);
|
||||
}
|
||||
|
||||
@ -292,8 +296,8 @@ async function signInUserWithQrCode(
|
||||
return migratedResult.authorization.user;
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'SESSION_PASSWORD_NEEDED') {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'SESSION_PASSWORD_NEEDED') {
|
||||
return signInWithPassword(client, apiCredentials, authParams);
|
||||
}
|
||||
|
||||
@ -345,8 +349,8 @@ async function sendCode(
|
||||
phoneCodeHash: resendResult.phoneCodeHash,
|
||||
isCodeViaApp: resendResult.type instanceof Api.auth.SentCodeTypeApp,
|
||||
};
|
||||
} catch (err: any) {
|
||||
if (err.message === 'AUTH_RESTART') {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && err.errorMessage === 'AUTH_RESTART') {
|
||||
return sendCode(client, apiCredentials, phoneNumber, forceSMS);
|
||||
} else {
|
||||
throw err;
|
||||
|
||||
@ -4,12 +4,13 @@ import type TelegramClient from './TelegramClient';
|
||||
|
||||
import Deferred from '../../../util/Deferred';
|
||||
import { Foreman } from '../../../util/foreman';
|
||||
import errors from '../errors';
|
||||
import { FloodPremiumWaitError, FloodWaitError, RPCError } from '../errors';
|
||||
import Api from '../tl/api';
|
||||
|
||||
import LocalUpdatePremiumFloodWait from '../../../api/gramjs/updates/UpdatePremiumFloodWait';
|
||||
import { sleep } from '../Helpers';
|
||||
import { getDownloadPartSize } from '../Utils';
|
||||
import type { SizeType } from './TelegramClient';
|
||||
|
||||
interface OnProgress {
|
||||
isCanceled?: boolean;
|
||||
@ -20,8 +21,7 @@ interface OnProgress {
|
||||
}
|
||||
|
||||
export interface DownloadFileParams {
|
||||
dcId: number;
|
||||
fileSize: number;
|
||||
fileSize?: number;
|
||||
workers?: number;
|
||||
partSizeKb?: number;
|
||||
start?: number;
|
||||
@ -30,6 +30,13 @@ export interface DownloadFileParams {
|
||||
isPriority?: boolean;
|
||||
}
|
||||
|
||||
export type DownloadFileWithDcParams = DownloadFileParams & { dcId: number };
|
||||
|
||||
export interface DownloadMediaParams {
|
||||
sizeType?: SizeType;
|
||||
progressCallback?: OnProgress;
|
||||
}
|
||||
|
||||
// Chunk sizes for `upload.getFile` must be multiple of the smallest size
|
||||
const MIN_CHUNK_SIZE = 4096;
|
||||
const DEFAULT_CHUNK_SIZE = 64; // kb
|
||||
@ -87,7 +94,7 @@ class FileView {
|
||||
}
|
||||
}
|
||||
|
||||
getData(): Promise<Buffer | File> {
|
||||
async getData(): Promise<Buffer | File> {
|
||||
if (this.type === 'opfs') {
|
||||
return this.largeFile!.getFile();
|
||||
} else {
|
||||
@ -98,19 +105,19 @@ class FileView {
|
||||
|
||||
export async function downloadFile(
|
||||
client: TelegramClient,
|
||||
inputLocation: Api.InputFileLocation,
|
||||
fileParams: DownloadFileParams,
|
||||
inputLocation: Api.TypeInputFileLocation,
|
||||
fileParams: DownloadFileWithDcParams,
|
||||
shouldDebugExportedSenders?: boolean,
|
||||
) {
|
||||
const { dcId } = fileParams;
|
||||
for (let i = 0; i < SENDER_RETRIES; i++) {
|
||||
try {
|
||||
return await downloadFile2(client, inputLocation, fileParams, shouldDebugExportedSenders);
|
||||
} catch (err: any) {
|
||||
if (
|
||||
(err.message.startsWith('SESSION_REVOKED') || err.message.startsWith('CONNECTION_NOT_INITED'))
|
||||
&& i < SENDER_RETRIES - 1
|
||||
) {
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof RPCError && (
|
||||
err.errorMessage.startsWith('SESSION_REVOKED')
|
||||
|| err.errorMessage.startsWith('CONNECTION_NOT_INITED')
|
||||
) && i < SENDER_RETRIES - 1) {
|
||||
await client._cleanupExportedSenders(dcId);
|
||||
} else {
|
||||
throw err;
|
||||
@ -131,12 +138,12 @@ const foremans = Array(MAX_CONCURRENT_CONNECTIONS_PREMIUM).fill(undefined)
|
||||
|
||||
async function downloadFile2(
|
||||
client: TelegramClient,
|
||||
inputLocation: Api.InputFileLocation,
|
||||
fileParams: DownloadFileParams,
|
||||
inputLocation: Api.TypeInputFileLocation,
|
||||
fileParams: DownloadFileWithDcParams,
|
||||
shouldDebugExportedSenders?: boolean,
|
||||
) {
|
||||
let {
|
||||
partSizeKb, end,
|
||||
partSizeKb, end = 0,
|
||||
} = fileParams;
|
||||
const {
|
||||
fileSize, dcId, progressCallback, isPriority, start = 0,
|
||||
@ -152,14 +159,18 @@ async function downloadFile2(
|
||||
logWithId('Downloading file...');
|
||||
const isPremium = Boolean(client.isPremium);
|
||||
|
||||
end = end && end < fileSize ? end : fileSize - 1;
|
||||
if (fileSize) {
|
||||
end = end && end < fileSize ? end : fileSize - 1;
|
||||
}
|
||||
|
||||
const rangeSize = end ? end - start + 1 : undefined;
|
||||
|
||||
if (!partSizeKb) {
|
||||
partSizeKb = fileSize ? getDownloadPartSize(start ? (end - start + 1) : fileSize) : DEFAULT_CHUNK_SIZE;
|
||||
partSizeKb = fileSize ? getDownloadPartSize(rangeSize || fileSize) : DEFAULT_CHUNK_SIZE;
|
||||
}
|
||||
|
||||
const partSize = partSizeKb * 1024;
|
||||
const partsCount = end ? Math.ceil((end + 1 - start + 1) / partSize) : 1;
|
||||
const partsCount = rangeSize ? Math.ceil(rangeSize / partSize) : 1;
|
||||
const noParallel = !end;
|
||||
const shouldUseMultipleConnections = Boolean(fileSize)
|
||||
&& fileSize >= MULTIPLE_CONNECTIONS_MIN_FILE_SIZE
|
||||
@ -172,7 +183,7 @@ async function downloadFile2(
|
||||
|
||||
client._log.info(`Downloading file in chunks of ${partSize} bytes`);
|
||||
|
||||
const fileView = new FileView(end - start + 1);
|
||||
const fileView = new FileView(rangeSize);
|
||||
const promises: Promise<any>[] = [];
|
||||
let offset = start;
|
||||
// Used for files with unknown size and for manual cancellations
|
||||
@ -243,7 +254,7 @@ async function downloadFile2(
|
||||
}, 6000);
|
||||
}
|
||||
// sometimes a session is revoked and will cause this to hang.
|
||||
const result = await Promise.race([
|
||||
const result = (await Promise.race([
|
||||
sender.send(new Api.upload.GetFile({
|
||||
location: inputLocation,
|
||||
offset: BigInt(offsetMemo),
|
||||
@ -260,9 +271,13 @@ async function downloadFile2(
|
||||
return Promise.reject(new Error('SESSION_REVOKED'));
|
||||
}
|
||||
}),
|
||||
]);
|
||||
]))!;
|
||||
client.releaseExportedSender(sender);
|
||||
|
||||
if (result instanceof Api.upload.FileCdnRedirect) {
|
||||
throw new Error('CDN download not supported');
|
||||
}
|
||||
|
||||
isDone2 = true;
|
||||
if (progressCallback) {
|
||||
if (progressCallback.isCanceled) {
|
||||
@ -288,8 +303,8 @@ async function downloadFile2(
|
||||
if (sender && !sender.isConnected()) {
|
||||
await sleep(DISCONNECT_SLEEP);
|
||||
continue;
|
||||
} else if (err instanceof errors.FloodWaitError) {
|
||||
if (err instanceof errors.FloodPremiumWaitError && !isPremiumFloodWaitSent) {
|
||||
} else if (err instanceof FloodWaitError) {
|
||||
if (err instanceof FloodPremiumWaitError && !isPremiumFloodWaitSent) {
|
||||
sender?._updateCallback(new LocalUpdatePremiumFloodWait(false));
|
||||
isPremiumFloodWaitSent = true;
|
||||
}
|
||||
@ -302,7 +317,7 @@ async function downloadFile2(
|
||||
if (deferred) deferred.resolve();
|
||||
|
||||
hasEnded = true;
|
||||
client.releaseExportedSender(sender);
|
||||
if (sender) client.releaseExportedSender(sender);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type TelegramClient from './TelegramClient';
|
||||
|
||||
import { Foreman } from '../../../util/foreman';
|
||||
import errors from '../errors';
|
||||
import { FloodPremiumWaitError, FloodWaitError } from '../errors';
|
||||
import Api from '../tl/api';
|
||||
|
||||
import LocalUpdatePremiumFloodWait from '../../../api/gramjs/updates/UpdatePremiumFloodWait';
|
||||
@ -133,8 +133,8 @@ export async function uploadFile(
|
||||
if (sender && !sender.isConnected()) {
|
||||
await sleep(DISCONNECT_SLEEP);
|
||||
continue;
|
||||
} else if (err instanceof errors.FloodWaitError) {
|
||||
if (err instanceof errors.FloodPremiumWaitError && !isPremiumFloodWaitSent) {
|
||||
} else if (err instanceof FloodWaitError) {
|
||||
if (err instanceof FloodPremiumWaitError && !isPremiumFloodWaitSent) {
|
||||
sender?._updateCallback(new LocalUpdatePremiumFloodWait(true));
|
||||
isPremiumFloodWaitSent = true;
|
||||
}
|
||||
@ -142,7 +142,7 @@ export async function uploadFile(
|
||||
continue;
|
||||
}
|
||||
foremans[senderIndex].releaseWorker();
|
||||
client.releaseExportedSender(sender);
|
||||
if (sender) client.releaseExportedSender(sender);
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -1,14 +1,25 @@
|
||||
const {
|
||||
sha1,
|
||||
toSignedLittleBuffer,
|
||||
readBufferFromBigInt,
|
||||
readBigIntFromBuffer,
|
||||
} = require('../Helpers');
|
||||
const BinaryReader = require('../extensions/BinaryReader');
|
||||
const { sleep } = require('../Helpers');
|
||||
import type BigInt from 'big-integer';
|
||||
|
||||
class AuthKey {
|
||||
constructor(value, hash) {
|
||||
import { BinaryReader } from '../extensions';
|
||||
|
||||
import {
|
||||
readBigIntFromBuffer,
|
||||
readBufferFromBigInt,
|
||||
sha1,
|
||||
sleep,
|
||||
toSignedLittleBuffer,
|
||||
} from '../Helpers';
|
||||
|
||||
export class AuthKey {
|
||||
_key?: Buffer;
|
||||
|
||||
_hash?: Buffer;
|
||||
|
||||
private auxHash?: BigInt.BigInteger;
|
||||
|
||||
keyId?: BigInt.BigInteger;
|
||||
|
||||
constructor(value?: Buffer, hash?: Buffer) {
|
||||
if (!hash || !value) {
|
||||
return;
|
||||
}
|
||||
@ -20,7 +31,7 @@ class AuthKey {
|
||||
this.keyId = reader.readLong(false);
|
||||
}
|
||||
|
||||
async setKey(value) {
|
||||
async setKey(value?: Buffer | AuthKey) {
|
||||
if (!value) {
|
||||
this._key = undefined;
|
||||
this.auxHash = undefined;
|
||||
@ -59,24 +70,35 @@ class AuthKey {
|
||||
* Calculates the new nonce hash based on the current class fields' values
|
||||
* @param newNonce
|
||||
* @param number
|
||||
* @returns {bigint}
|
||||
* @returns {BigInt.BigInteger}
|
||||
*/
|
||||
async calcNewNonceHash(newNonce, number) {
|
||||
newNonce = toSignedLittleBuffer(newNonce, 32);
|
||||
async calcNewNonceHash(
|
||||
newNonce: BigInt.BigInteger,
|
||||
number: number,
|
||||
): Promise<BigInt.BigInteger> {
|
||||
if (!this.auxHash) {
|
||||
throw new Error('Auth key not set');
|
||||
}
|
||||
|
||||
const nonce = toSignedLittleBuffer(newNonce, 32);
|
||||
const n = Buffer.alloc(1);
|
||||
n.writeUInt8(number, 0);
|
||||
const data = Buffer.concat([newNonce,
|
||||
Buffer.concat([n, readBufferFromBigInt(this.auxHash, 8, true)])]);
|
||||
const data = Buffer.concat([
|
||||
nonce,
|
||||
Buffer.concat([n, readBufferFromBigInt(this.auxHash, 8, true)]),
|
||||
]);
|
||||
|
||||
// Calculates the message key from the given data
|
||||
const shaData = (await sha1(data)).slice(4, 20);
|
||||
return readBigIntFromBuffer(shaData, true, true);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return other instanceof this.constructor && this._key && other.getKey() && other.getKey()
|
||||
.equals(this._key);
|
||||
equals(other: AuthKey) {
|
||||
return (
|
||||
other instanceof this.constructor
|
||||
&& this._key
|
||||
&& Buffer.isBuffer(other.getKey())
|
||||
&& other.getKey()?.equals(this._key)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthKey;
|
||||
@ -1,17 +0,0 @@
|
||||
const crypto = require('./crypto');
|
||||
|
||||
class CTR {
|
||||
constructor(key, iv) {
|
||||
if (!Buffer.isBuffer(key) || !Buffer.isBuffer(iv) || iv.length !== 16) {
|
||||
throw new Error('Key and iv need to be a buffer');
|
||||
}
|
||||
|
||||
this.cipher = crypto.createCipheriv('AES-256-CTR', key, iv);
|
||||
}
|
||||
|
||||
encrypt(data) {
|
||||
return Buffer.from(this.cipher.update(data));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CTR;
|
||||
23
src/lib/gramjs/crypto/CTR.ts
Normal file
23
src/lib/gramjs/crypto/CTR.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createCipheriv, createDecipheriv, type CtrImpl } from './crypto';
|
||||
|
||||
export class CTR {
|
||||
private cipher: CtrImpl;
|
||||
private decipher: CtrImpl;
|
||||
|
||||
constructor(key: Buffer, iv: Buffer) {
|
||||
if (!Buffer.isBuffer(key) || !Buffer.isBuffer(iv) || iv.length !== 16) {
|
||||
throw new Error('Key and iv need to be a buffer');
|
||||
}
|
||||
|
||||
this.cipher = createCipheriv('AES-256-CTR', key, iv);
|
||||
this.decipher = createDecipheriv('AES-256-CTR', key, iv);
|
||||
}
|
||||
|
||||
encrypt(data: Buffer) {
|
||||
return Buffer.from(this.cipher.update(data));
|
||||
}
|
||||
|
||||
decrypt(data: Buffer) {
|
||||
return Buffer.from(this.decipher.update(data));
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,15 @@
|
||||
const BigInt = require('big-integer');
|
||||
const { modExp } = require('../Helpers');
|
||||
import BigInt from 'big-integer';
|
||||
|
||||
class Factorizator {
|
||||
import { modExp } from '../Helpers';
|
||||
|
||||
export class Factorizator {
|
||||
/**
|
||||
* Calculates the greatest common divisor
|
||||
* @param a {BigInteger}
|
||||
* @param b {BigInteger}
|
||||
* @returns {BigInteger}
|
||||
*/
|
||||
static gcd(a, b) {
|
||||
static gcd(a: BigInt.BigInteger, b: BigInt.BigInteger) {
|
||||
while (b.neq(BigInt.zero)) {
|
||||
const temp = b;
|
||||
b = a.remainder(b);
|
||||
@ -22,13 +23,9 @@ class Factorizator {
|
||||
* @param pq {BigInteger}
|
||||
* @returns {{p: *, q: *}}
|
||||
*/
|
||||
static factorize(pq) {
|
||||
if (pq.remainder(2)
|
||||
.equals(BigInt.zero)) {
|
||||
return {
|
||||
p: BigInt(2),
|
||||
q: pq.divide(BigInt(2)),
|
||||
};
|
||||
static factorize(pq: BigInt.BigInteger) {
|
||||
if (pq.remainder(2).equals(BigInt.zero)) {
|
||||
return { p: BigInt(2), q: pq.divide(BigInt(2)) };
|
||||
}
|
||||
let y = BigInt.randBetween(BigInt(1), pq.minus(1));
|
||||
const c = BigInt.randBetween(BigInt(1), pq.minus(1));
|
||||
@ -43,23 +40,17 @@ class Factorizator {
|
||||
|
||||
while (g.eq(BigInt.one)) {
|
||||
x = y;
|
||||
for (let i = 0; BigInt(i)
|
||||
.lesser(r); i++) {
|
||||
y = (modExp(y, BigInt(2), pq)).add(c)
|
||||
.remainder(pq);
|
||||
for (let i = 0; BigInt(i).lesser(r); i++) {
|
||||
y = modExp(y, BigInt(2), pq).add(c).remainder(pq);
|
||||
}
|
||||
k = BigInt.zero;
|
||||
|
||||
while (k.lesser(r) && g.eq(BigInt.one)) {
|
||||
ys = y;
|
||||
const condition = BigInt.min(m, r.minus(k));
|
||||
for (let i = 0; BigInt(i)
|
||||
.lesser(condition); i++) {
|
||||
y = (modExp(y, BigInt(2), pq)).add(c)
|
||||
.remainder(pq);
|
||||
q = q.multiply(x.minus(y)
|
||||
.abs())
|
||||
.remainder(pq);
|
||||
for (let i = 0; BigInt(i).lesser(condition); i++) {
|
||||
y = modExp(y, BigInt(2), pq).add(c).remainder(pq);
|
||||
q = q.multiply(x.minus(y).abs()).remainder(pq);
|
||||
}
|
||||
g = Factorizator.gcd(q, pq);
|
||||
k = k.add(m);
|
||||
@ -69,12 +60,9 @@ class Factorizator {
|
||||
}
|
||||
|
||||
if (g.eq(pq)) {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
ys = (modExp(ys, BigInt(2), pq)).add(c)
|
||||
.remainder(pq);
|
||||
g = Factorizator.gcd(x.minus(ys)
|
||||
.abs(), pq);
|
||||
ys = modExp(ys, BigInt(2), pq).add(c).remainder(pq);
|
||||
g = Factorizator.gcd(x.minus(ys).abs(), pq);
|
||||
|
||||
if (g.greater(1)) {
|
||||
break;
|
||||
@ -83,14 +71,6 @@ class Factorizator {
|
||||
}
|
||||
const p = g;
|
||||
q = pq.divide(g);
|
||||
return p < q ? {
|
||||
p,
|
||||
q,
|
||||
} : {
|
||||
p: q,
|
||||
q: p,
|
||||
};
|
||||
return p < q ? { p, q } : { p: q, q: p };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Factorizator;
|
||||
@ -1,33 +0,0 @@
|
||||
const { IGE: AESIGE } = require('@cryptography/aes');
|
||||
const Helpers = require('../Helpers');
|
||||
|
||||
class IGENEW {
|
||||
constructor(key, iv) {
|
||||
this.ige = new AESIGE(key, iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector
|
||||
* @param cipherText {Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
decryptIge(cipherText) {
|
||||
return Helpers.convertToLittle(this.ige.decrypt(cipherText));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector
|
||||
* @param plainText {Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
encryptIge(plainText) {
|
||||
const padding = plainText.length % 16;
|
||||
if (padding) {
|
||||
plainText = Buffer.concat([plainText, Helpers.generateRandomBytes(16 - padding)]);
|
||||
}
|
||||
|
||||
return Helpers.convertToLittle(this.ige.encrypt(plainText));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IGENEW;
|
||||
39
src/lib/gramjs/crypto/IGE.ts
Normal file
39
src/lib/gramjs/crypto/IGE.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { IGE as AesIge } from '@cryptography/aes';
|
||||
|
||||
import { convertToLittle, generateRandomBytes } from '../Helpers';
|
||||
|
||||
class IGENEW {
|
||||
private ige: AesIge;
|
||||
|
||||
constructor(key: Buffer, iv: Buffer) {
|
||||
this.ige = new AesIge(key, iv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector
|
||||
* @param cipherText {Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
decryptIge(cipherText: Buffer): Buffer {
|
||||
return convertToLittle(this.ige.decrypt(cipherText));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the given text in 16-bytes blocks by using the given key and 32-bytes initialization vector
|
||||
* @param plainText {Buffer}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
encryptIge(plainText: Buffer): Buffer {
|
||||
const padding = plainText.length % 16;
|
||||
if (padding) {
|
||||
plainText = Buffer.concat([
|
||||
plainText,
|
||||
generateRandomBytes(16 - padding),
|
||||
]);
|
||||
}
|
||||
|
||||
return convertToLittle(this.ige.encrypt(plainText));
|
||||
}
|
||||
}
|
||||
|
||||
export { IGENEW as IGE };
|
||||
@ -1,18 +1,13 @@
|
||||
const AES = require('@cryptography/aes').default;
|
||||
const {
|
||||
i2ab,
|
||||
ab2i,
|
||||
} = require('./converters');
|
||||
const { getWords } = require('./words');
|
||||
import AES from '@cryptography/aes';
|
||||
|
||||
import { ab2i, i2ab } from './converters';
|
||||
import { getWords } from './words';
|
||||
|
||||
class Counter {
|
||||
constructor(initialValue) {
|
||||
this.setBytes(initialValue);
|
||||
}
|
||||
_counter: Buffer;
|
||||
|
||||
setBytes(bytes) {
|
||||
bytes = Buffer.from(bytes);
|
||||
this._counter = bytes;
|
||||
constructor(initialValue: Buffer) {
|
||||
this._counter = Buffer.from(initialValue);
|
||||
}
|
||||
|
||||
increment() {
|
||||
@ -28,7 +23,15 @@ class Counter {
|
||||
}
|
||||
|
||||
class CTR {
|
||||
constructor(key, counter) {
|
||||
private _counter: Counter;
|
||||
|
||||
private _remainingCounter?: Buffer;
|
||||
|
||||
private _remainingCounterIndex: number;
|
||||
|
||||
private _aes: AES;
|
||||
|
||||
constructor(key: Buffer, counter: Counter | Buffer) {
|
||||
if (!(counter instanceof Counter)) {
|
||||
counter = new Counter(counter);
|
||||
}
|
||||
@ -41,11 +44,11 @@ class CTR {
|
||||
this._aes = new AES(getWords(key));
|
||||
}
|
||||
|
||||
update(plainText) {
|
||||
update(plainText: Buffer) {
|
||||
return this.encrypt(plainText);
|
||||
}
|
||||
|
||||
encrypt(plainText) {
|
||||
encrypt(plainText: Buffer) {
|
||||
const encrypted = Buffer.from(plainText);
|
||||
|
||||
for (let i = 0; i < encrypted.length; i++) {
|
||||
@ -54,15 +57,19 @@ class CTR {
|
||||
this._remainingCounterIndex = 0;
|
||||
this._counter.increment();
|
||||
}
|
||||
encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++];
|
||||
if (this._remainingCounter) {
|
||||
encrypted[i] ^= this._remainingCounter[this._remainingCounterIndex++];
|
||||
}
|
||||
}
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
}
|
||||
|
||||
export type CtrImpl = CTR;
|
||||
|
||||
// endregion
|
||||
function createDecipheriv(algorithm, key, iv) {
|
||||
export function createDecipheriv(algorithm: string, key: Buffer, iv: Buffer) {
|
||||
if (algorithm.includes('ECB')) {
|
||||
throw new Error('Not supported');
|
||||
} else {
|
||||
@ -70,7 +77,7 @@ function createDecipheriv(algorithm, key, iv) {
|
||||
}
|
||||
}
|
||||
|
||||
function createCipheriv(algorithm, key, iv) {
|
||||
export function createCipheriv(algorithm: string, key: Buffer, iv: Buffer) {
|
||||
if (algorithm.includes('ECB')) {
|
||||
throw new Error('Not supported');
|
||||
} else {
|
||||
@ -78,18 +85,18 @@ function createCipheriv(algorithm, key, iv) {
|
||||
}
|
||||
}
|
||||
|
||||
function randomBytes(count) {
|
||||
export function randomBytes(count: number) {
|
||||
const bytes = new Uint8Array(count);
|
||||
crypto.getRandomValues(bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
class Hash {
|
||||
constructor(algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
private data = new Uint8Array(0);
|
||||
|
||||
update(data) {
|
||||
constructor(private algorithm: 'sha1' | 'sha256') {}
|
||||
|
||||
update(data: ArrayLike<number>) {
|
||||
// We shouldn't be needing new Uint8Array but it doesn't
|
||||
// work without it
|
||||
this.data = new Uint8Array(data);
|
||||
@ -99,15 +106,14 @@ class Hash {
|
||||
if (this.algorithm === 'sha1') {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
return Buffer.from(await self.crypto.subtle.digest('SHA-1', this.data));
|
||||
} else if (this.algorithm === 'sha256') {
|
||||
} else {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
return Buffer.from(await self.crypto.subtle.digest('SHA-256', this.data));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function pbkdf2(password, salt, iterations) {
|
||||
export async function pbkdf2(password: Buffer, salt: Buffer, iterations: number) {
|
||||
const passwordKey = await crypto.subtle.importKey('raw', password, { name: 'PBKDF2' }, false, ['deriveBits']);
|
||||
return Buffer.from(await crypto.subtle.deriveBits({
|
||||
name: 'PBKDF2',
|
||||
@ -117,14 +123,6 @@ async function pbkdf2(password, salt, iterations) {
|
||||
}, passwordKey, 512));
|
||||
}
|
||||
|
||||
function createHash(algorithm) {
|
||||
export function createHash(algorithm: 'sha1' | 'sha256') {
|
||||
return new Hash(algorithm);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
randomBytes,
|
||||
createHash,
|
||||
pbkdf2,
|
||||
};
|
||||
@ -2,10 +2,12 @@
|
||||
* Errors not related to the Telegram API itself
|
||||
*/
|
||||
|
||||
import type { Api } from '../tl';
|
||||
|
||||
/**
|
||||
* Occurs when a read operation was cancelled.
|
||||
*/
|
||||
class ReadCancelledError extends Error {
|
||||
export class ReadCancelledError extends Error {
|
||||
constructor() {
|
||||
super('The read operation was cancelled.');
|
||||
}
|
||||
@ -15,8 +17,12 @@ class ReadCancelledError extends Error {
|
||||
* Occurs when a type is not found, for example,
|
||||
* when trying to read a TLObject with an invalid constructor code.
|
||||
*/
|
||||
class TypeNotFoundError extends Error {
|
||||
constructor(invalidConstructorId, remaining) {
|
||||
export class TypeNotFoundError extends Error {
|
||||
invalidConstructorId: number;
|
||||
|
||||
remaining: Buffer;
|
||||
|
||||
constructor(invalidConstructorId: number, remaining: Buffer) {
|
||||
super(`Could not find a matching Constructor ID for the TLObject that was supposed to be
|
||||
read with ID ${invalidConstructorId}. Most likely, a TLObject was trying to be read when
|
||||
it should not be read. Remaining bytes: ${remaining.length}`);
|
||||
@ -33,8 +39,12 @@ class TypeNotFoundError extends Error {
|
||||
* Occurs when using the TCP full mode and the checksum of a received
|
||||
* packet doesn't match the expected checksum.
|
||||
*/
|
||||
class InvalidChecksumError extends Error {
|
||||
constructor(checksum, validChecksum) {
|
||||
export class InvalidChecksumError extends Error {
|
||||
checksum: number;
|
||||
|
||||
validChecksum: number;
|
||||
|
||||
constructor(checksum: number, validChecksum: number) {
|
||||
super(`Invalid checksum (${checksum} when ${validChecksum} was expected). This packet should be skipped.`);
|
||||
this.checksum = checksum;
|
||||
this.validChecksum = validChecksum;
|
||||
@ -45,8 +55,12 @@ class InvalidChecksumError extends Error {
|
||||
* Occurs when the buffer is invalid, and may contain an HTTP error code.
|
||||
* For instance, 404 means "forgotten/broken authorization key", while
|
||||
*/
|
||||
class InvalidBufferError extends Error {
|
||||
constructor(payload) {
|
||||
export class InvalidBufferError extends Error {
|
||||
code?: number;
|
||||
|
||||
payload: Buffer;
|
||||
|
||||
constructor(payload: Buffer) {
|
||||
let code;
|
||||
if (payload.length === 4) {
|
||||
code = -payload.readInt32LE(0);
|
||||
@ -62,8 +76,8 @@ class InvalidBufferError extends Error {
|
||||
/**
|
||||
* Generic security error, mostly used when generating a new AuthKey.
|
||||
*/
|
||||
class SecurityError extends Error {
|
||||
constructor(...args) {
|
||||
export class SecurityError extends Error {
|
||||
constructor(...args: any[]) {
|
||||
if (!args.length) {
|
||||
args = ['A security check failed.'];
|
||||
}
|
||||
@ -75,7 +89,7 @@ class SecurityError extends Error {
|
||||
* Occurs when there's a hash mismatch between the decrypted CDN file
|
||||
* and its expected hash.
|
||||
*/
|
||||
class CdnFileTamperedError extends SecurityError {
|
||||
export class CdnFileTamperedError extends SecurityError {
|
||||
constructor() {
|
||||
super('The CDN file has been altered and its download cancelled.');
|
||||
}
|
||||
@ -84,8 +98,8 @@ class CdnFileTamperedError extends SecurityError {
|
||||
/**
|
||||
* Occurs when handling a badMessageNotification
|
||||
*/
|
||||
class BadMessageError extends Error {
|
||||
static ErrorMessages = {
|
||||
export class BadMessageError extends Error {
|
||||
static ErrorMessages: Record<number, string> = {
|
||||
16:
|
||||
'msg_id too low (most likely, client time is wrong it would be worthwhile to '
|
||||
+ 'synchronize it using msg_id notifications and re-send the original message '
|
||||
@ -125,24 +139,18 @@ class BadMessageError extends Error {
|
||||
64: 'Invalid container.',
|
||||
};
|
||||
|
||||
constructor(request, code) {
|
||||
code: number;
|
||||
|
||||
errorMessage: string;
|
||||
|
||||
constructor(request: Api.AnyRequest, code: number) {
|
||||
let errorMessage = BadMessageError.ErrorMessages[code]
|
||||
|| `Unknown error code (this should not happen): ${code}.`;
|
||||
errorMessage += ` Caused by ${request.className}`;
|
||||
super(errorMessage);
|
||||
this.message = errorMessage;
|
||||
this.errorMessage = errorMessage;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO : Support multi errors.
|
||||
|
||||
module.exports = {
|
||||
ReadCancelledError,
|
||||
TypeNotFoundError,
|
||||
InvalidChecksumError,
|
||||
InvalidBufferError,
|
||||
SecurityError,
|
||||
CdnFileTamperedError,
|
||||
BadMessageError,
|
||||
};
|
||||
@ -1,19 +1,25 @@
|
||||
import type { Api } from '../tl';
|
||||
|
||||
/**
|
||||
* Base class for all Remote Procedure Call errors.
|
||||
*/
|
||||
class RPCError extends Error {
|
||||
constructor(message, request, code = undefined) {
|
||||
export class RPCError extends Error {
|
||||
public code: number | undefined;
|
||||
|
||||
public errorMessage: string;
|
||||
|
||||
constructor(message: string, request: Api.AnyRequest, code?: number) {
|
||||
super(
|
||||
'RPCError {0}: {1}{2}'
|
||||
.replace('{0}', code)
|
||||
.replace('{0}', code?.toString() || '')
|
||||
.replace('{1}', message)
|
||||
.replace('{2}', RPCError._fmtRequest(request)),
|
||||
);
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.errorMessage = message;
|
||||
}
|
||||
|
||||
static _fmtRequest(request) {
|
||||
static _fmtRequest(request: Api.AnyRequest) {
|
||||
// TODO fix this
|
||||
if (request) {
|
||||
return ` (caused by ${request.className})`;
|
||||
@ -26,11 +32,11 @@ class RPCError extends Error {
|
||||
/**
|
||||
* The request must be repeated, but directed to a different data center.
|
||||
*/
|
||||
class InvalidDCError extends RPCError {
|
||||
constructor(request, message, code) {
|
||||
export class InvalidDCError extends RPCError {
|
||||
constructor(message: string, request: Api.AnyRequest, code?: number) {
|
||||
super(message, request, code);
|
||||
this.code = code || 303;
|
||||
this.message = message || 'ERROR_SEE_OTHER';
|
||||
this.errorMessage = message || 'ERROR_SEE_OTHER';
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,49 +45,49 @@ class InvalidDCError extends RPCError {
|
||||
* using a form and contains user generated data, the user should be
|
||||
* notified that the data must be corrected before the query is repeated.
|
||||
*/
|
||||
class BadRequestError extends RPCError {
|
||||
export class BadRequestError extends RPCError {
|
||||
code = 400;
|
||||
|
||||
message = 'BAD_REQUEST';
|
||||
errorMessage = 'BAD_REQUEST';
|
||||
}
|
||||
|
||||
/**
|
||||
* There was an unauthorized attempt to use functionality available only
|
||||
* to authorized users.
|
||||
*/
|
||||
class UnauthorizedError extends RPCError {
|
||||
export class UnauthorizedError extends RPCError {
|
||||
code = 401;
|
||||
|
||||
message = 'UNAUTHORIZED';
|
||||
errorMessage = 'UNAUTHORIZED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Privacy violation. For example, an attempt to write a message to
|
||||
* someone who has blacklisted the current user.
|
||||
*/
|
||||
class ForbiddenError extends RPCError {
|
||||
export class ForbiddenError extends RPCError {
|
||||
code = 403;
|
||||
|
||||
message = 'FORBIDDEN';
|
||||
errorMessage = 'FORBIDDEN';
|
||||
}
|
||||
|
||||
/**
|
||||
* An attempt to invoke a non-existent object, such as a method.
|
||||
*/
|
||||
class NotFoundError extends RPCError {
|
||||
export class NotFoundError extends RPCError {
|
||||
code = 404;
|
||||
|
||||
message = 'NOT_FOUND';
|
||||
errorMessage = 'NOT_FOUND';
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors related to invalid authorization key, like
|
||||
* AUTH_KEY_DUPLICATED which can cause the connection to fail.
|
||||
*/
|
||||
class AuthKeyError extends RPCError {
|
||||
export class AuthKeyError extends RPCError {
|
||||
code = 406;
|
||||
|
||||
message = 'AUTH_KEY';
|
||||
errorMessage = 'AUTH_KEY';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,10 +96,10 @@ class AuthKeyError extends RPCError {
|
||||
* attempt to request a large number of text messages (SMS) for the same
|
||||
* phone number.
|
||||
*/
|
||||
class FloodError extends RPCError {
|
||||
export class FloodError extends RPCError {
|
||||
code = 420;
|
||||
|
||||
message = 'FLOOD';
|
||||
errorMessage = 'FLOOD';
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,31 +107,18 @@ class FloodError extends RPCError {
|
||||
* for example, there was a disruption while accessing a database or file
|
||||
* storage
|
||||
*/
|
||||
class ServerError extends RPCError {
|
||||
export class ServerError extends RPCError {
|
||||
code = 500; // Also witnessed as -500
|
||||
|
||||
message = 'INTERNAL';
|
||||
errorMessage = 'INTERNAL';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicking the inline buttons of bots that never (or take to long to)
|
||||
* call ``answerCallbackQuery`` will result in this "special" RPCError.
|
||||
*/
|
||||
class TimedOutError extends RPCError {
|
||||
export class TimedOutError extends RPCError {
|
||||
code = 503; // Only witnessed as -503
|
||||
|
||||
message = 'Timeout';
|
||||
errorMessage = 'Timeout';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RPCError,
|
||||
InvalidDCError,
|
||||
BadRequestError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
AuthKeyError,
|
||||
FloodError,
|
||||
ServerError,
|
||||
TimedOutError,
|
||||
};
|
||||
@ -1,117 +1,154 @@
|
||||
const {
|
||||
RPCError,
|
||||
InvalidDCError,
|
||||
FloodError,
|
||||
BadRequestError,
|
||||
TimedOutError,
|
||||
} = require('./RPCBaseErrors');
|
||||
/* eslint-disable max-len */
|
||||
import {
|
||||
BadRequestError, FloodError, InvalidDCError, RPCError, TimedOutError,
|
||||
} from './RPCBaseErrors';
|
||||
|
||||
class UserMigrateError extends InvalidDCError {
|
||||
constructor(args) {
|
||||
export class UserMigrateError extends InvalidDCError {
|
||||
public newDc: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const newDc = Number(args.capture || 0);
|
||||
// eslint-disable-next-line max-len
|
||||
super(`The user whose identity is being used to execute queries is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`);
|
||||
// eslint-disable-next-line max-len
|
||||
super(`The user whose identity is being used to execute queries is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`, args.request);
|
||||
this.message = `The user whose identity is being used to execute queries is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`;
|
||||
this.newDc = newDc;
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneMigrateError extends InvalidDCError {
|
||||
constructor(args) {
|
||||
export class PhoneMigrateError extends InvalidDCError {
|
||||
public newDc: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const newDc = Number(args.capture || 0);
|
||||
// eslint-disable-next-line max-len
|
||||
super(`The phone number a user is trying to use for authorization is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`);
|
||||
// eslint-disable-next-line max-len
|
||||
super(`The phone number a user is trying to use for authorization is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`, args.request);
|
||||
this.message = `The phone number a user is trying to use for authorization is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`;
|
||||
this.newDc = newDc;
|
||||
}
|
||||
}
|
||||
|
||||
class SlowModeWaitError extends FloodError {
|
||||
constructor(args) {
|
||||
export class SlowModeWaitError extends FloodError {
|
||||
public seconds: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const seconds = Number(args.capture || 0);
|
||||
// eslint-disable-next-line max-len
|
||||
super(`A wait of ${seconds} seconds is required before sending another message in this chat${RPCError._fmtRequest(args.request)}`);
|
||||
// eslint-disable-next-line max-len
|
||||
super(
|
||||
`A wait of ${seconds} seconds is required before sending another message in this chat ${RPCError._fmtRequest(args.request)}`,
|
||||
args.request,
|
||||
);
|
||||
this.message = `A wait of ${seconds} seconds is required before sending another message in this chat${RPCError._fmtRequest(args.request)}`;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
class FloodWaitError extends FloodError {
|
||||
constructor(args) {
|
||||
export class FloodWaitError extends FloodError {
|
||||
public seconds: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const seconds = Number(args.capture || 0);
|
||||
super(`A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`);
|
||||
super(
|
||||
`A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`,
|
||||
args.request,
|
||||
);
|
||||
this.message = `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
}
|
||||
class FloodPremiumWaitError extends FloodWaitError {
|
||||
constructor(args) {
|
||||
const seconds = Number(args.capture || 0);
|
||||
super(`A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`);
|
||||
this.message = `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
}
|
||||
class MsgWaitError extends FloodError {
|
||||
constructor(args) {
|
||||
super(`Message failed to be sent.${RPCError._fmtRequest(args.request)}`);
|
||||
this.message = `Message failed to be sent.${RPCError._fmtRequest(args.request)}`;
|
||||
}
|
||||
}
|
||||
|
||||
class FloodTestPhoneWaitError extends FloodError {
|
||||
constructor(args) {
|
||||
export class FloodPremiumWaitError extends FloodWaitError {
|
||||
constructor(args: any) {
|
||||
const seconds = Number(args.capture || 0);
|
||||
super(`A wait of ${seconds} seconds is required in the test servers${RPCError._fmtRequest(args.request)}`);
|
||||
// eslint-disable-next-line max-len
|
||||
super(`A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`);
|
||||
this.message = `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
export class MsgWaitError extends FloodError {
|
||||
constructor(args: any) {
|
||||
super(
|
||||
`Message failed to be sent.${RPCError._fmtRequest(args.request)}`,
|
||||
args.request,
|
||||
);
|
||||
this.message = `Message failed to be sent.${RPCError._fmtRequest(
|
||||
args.request,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export class FloodTestPhoneWaitError extends FloodError {
|
||||
public seconds: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const seconds = Number(args.capture || 0);
|
||||
super(
|
||||
`A wait of ${seconds} seconds is required in the test servers${RPCError._fmtRequest(args.request)}`,
|
||||
args.request,
|
||||
);
|
||||
this.message = `A wait of ${seconds} seconds is required in the test servers${RPCError._fmtRequest(args.request)}`;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
class FileMigrateError extends InvalidDCError {
|
||||
constructor(args) {
|
||||
export class FileMigrateError extends InvalidDCError {
|
||||
public newDc: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const newDc = Number(args.capture || 0);
|
||||
super(`The file to be accessed is currently stored in DC ${newDc}${RPCError._fmtRequest(args.request)}`);
|
||||
// eslint-disable-next-line max-len
|
||||
super(
|
||||
`The file to be accessed is currently stored in DC ${newDc}${RPCError._fmtRequest(args.request)}`,
|
||||
args.request,
|
||||
);
|
||||
this.message = `The file to be accessed is currently stored in DC ${newDc}${RPCError._fmtRequest(args.request)}`;
|
||||
this.newDc = newDc;
|
||||
}
|
||||
}
|
||||
|
||||
class NetworkMigrateError extends InvalidDCError {
|
||||
constructor(args) {
|
||||
export class NetworkMigrateError extends InvalidDCError {
|
||||
public newDc: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const newDc = Number(args.capture || 0);
|
||||
super(`The source IP address is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`);
|
||||
super(
|
||||
`The source IP address is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`,
|
||||
args.request,
|
||||
);
|
||||
this.message = `The source IP address is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`;
|
||||
this.newDc = newDc;
|
||||
}
|
||||
}
|
||||
|
||||
class EmailUnconfirmedError extends BadRequestError {
|
||||
constructor(args) {
|
||||
export class EmailUnconfirmedError extends BadRequestError {
|
||||
codeLength: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const codeLength = Number(args.capture || 0);
|
||||
super(`Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(args.request)}`);
|
||||
super(
|
||||
`Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(
|
||||
args.request,
|
||||
)}`,
|
||||
args.request,
|
||||
400,
|
||||
);
|
||||
// eslint-disable-next-line max-len
|
||||
this.message = `Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(args.request)}`;
|
||||
this.message = `Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest(
|
||||
args.request,
|
||||
)}`;
|
||||
this.codeLength = codeLength;
|
||||
}
|
||||
}
|
||||
|
||||
class PasswordModifiedError extends BadRequestError {
|
||||
constructor(args) {
|
||||
export class PasswordModifiedError extends BadRequestError {
|
||||
public seconds: number;
|
||||
|
||||
constructor(args: any) {
|
||||
const seconds = Number(args.capture || 0);
|
||||
super(`The password was modified less than 24 hours ago, try again in ${seconds} seconds.`);
|
||||
super(`The password was modified less than 24 hours ago, try again in ${seconds} seconds.`, args.request);
|
||||
// eslint-disable-next-line max-len
|
||||
this.message = `The password was modified less than 24 hours ago, try again in ${seconds} seconds.`;
|
||||
this.seconds = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
const rpcErrorRe = [
|
||||
export const rpcErrorRe = new Map<RegExp, any>([
|
||||
[/FILE_MIGRATE_(\d+)/, FileMigrateError],
|
||||
[/FLOOD_TEST_PHONE_WAIT_(\d+)/, FloodTestPhoneWaitError],
|
||||
[/FLOOD_WAIT_(\d+)/, FloodWaitError],
|
||||
@ -124,18 +161,4 @@ const rpcErrorRe = [
|
||||
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
|
||||
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordModifiedError],
|
||||
[/^Timeout$/, TimedOutError],
|
||||
];
|
||||
module.exports = {
|
||||
rpcErrorRe,
|
||||
FileMigrateError,
|
||||
FloodTestPhoneWaitError,
|
||||
FloodWaitError,
|
||||
FloodPremiumWaitError,
|
||||
PhoneMigrateError,
|
||||
SlowModeWaitError,
|
||||
UserMigrateError,
|
||||
NetworkMigrateError,
|
||||
MsgWaitError,
|
||||
EmailUnconfirmedError,
|
||||
PasswordModifiedError,
|
||||
};
|
||||
]);
|
||||
@ -1,34 +0,0 @@
|
||||
/**
|
||||
* Converts a Telegram's RPC Error to a Python error.
|
||||
* @param rpcError the RPCError instance
|
||||
* @param request the request that caused this error
|
||||
* @constructor the RPCError as a Python exception that represents this error
|
||||
*/
|
||||
const { RPCError } = require('./RPCBaseErrors');
|
||||
const { rpcErrorRe } = require('./RPCErrorList');
|
||||
|
||||
function RPCMessageToError(rpcError, request) {
|
||||
for (const [msgRegex, Cls] of rpcErrorRe) {
|
||||
const m = rpcError.errorMessage.match(msgRegex);
|
||||
if (m) {
|
||||
const capture = m.length === 2 ? parseInt(m[1], 10) : undefined;
|
||||
return new Cls({
|
||||
request,
|
||||
capture,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new RPCError(rpcError.errorMessage, request);
|
||||
}
|
||||
|
||||
const Common = require('./Common');
|
||||
const RPCBaseErrors = require('./RPCBaseErrors');
|
||||
const RPCErrorList = require('./RPCErrorList');
|
||||
|
||||
module.exports = {
|
||||
RPCMessageToError,
|
||||
...Common,
|
||||
...RPCBaseErrors,
|
||||
...RPCErrorList,
|
||||
};
|
||||
28
src/lib/gramjs/errors/index.ts
Normal file
28
src/lib/gramjs/errors/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Converts a Telegram's RPC Error to a Python error.
|
||||
* @param rpcError the RPCError instance
|
||||
* @param request the request that caused this error
|
||||
* @constructor the RPCError as a Python exception that represents this error
|
||||
*/
|
||||
import type { Api } from '../tl';
|
||||
|
||||
import { RPCError } from './RPCBaseErrors';
|
||||
import { rpcErrorRe } from './RPCErrorList';
|
||||
|
||||
export function RPCMessageToError(
|
||||
rpcError: Api.RpcError,
|
||||
request: Api.AnyRequest,
|
||||
) {
|
||||
for (const [msgRegex, Cls] of rpcErrorRe) {
|
||||
const m = rpcError.errorMessage.match(msgRegex);
|
||||
if (m) {
|
||||
const capture = m.length === 2 ? parseInt(m[1], 10) : undefined;
|
||||
return new Cls({ request, capture });
|
||||
}
|
||||
}
|
||||
return new RPCError(rpcError.errorMessage, request, rpcError.errorCode);
|
||||
}
|
||||
|
||||
export * from './Common';
|
||||
export * from './RPCBaseErrors';
|
||||
export * from './RPCErrorList';
|
||||
@ -1,94 +0,0 @@
|
||||
/* CONTEST
|
||||
const { EventBuilder, EventCommon } = require('./common')
|
||||
const { constructors } = require('../tl')
|
||||
|
||||
class NewMessage extends EventBuilder {
|
||||
constructor(args = {
|
||||
chats: null,
|
||||
func: null,
|
||||
}) {
|
||||
super(args)
|
||||
|
||||
this.chats = args.chats
|
||||
this.func = args.func
|
||||
this._noCheck = true
|
||||
}
|
||||
|
||||
async _resolve(client) {
|
||||
await super._resolve(client)
|
||||
// this.fromUsers = await _intoIdSet(client, this.fromUsers)
|
||||
}
|
||||
|
||||
build(update, others = null, thisId = null) {
|
||||
let event
|
||||
if (update instanceof constructors.UpdateNewMessage || update instanceof constructors.UpdateNewChannelMessage) {
|
||||
if (!(update.message instanceof constructors.Message)) {
|
||||
return
|
||||
}
|
||||
event = new Event(update.message)
|
||||
} else if (update instanceof constructors.UpdateShortMessage) {
|
||||
event = new Event(new constructors.Message({
|
||||
out: update.out,
|
||||
mentioned: update.mentioned,
|
||||
mediaUnread: update.mediaUnread,
|
||||
silent: update.silent,
|
||||
id: update.id,
|
||||
// Note that to_id/from_id complement each other in private
|
||||
// messages, depending on whether the message was outgoing.
|
||||
toId: new constructors.PeerUser(update.out ? update.userId : thisId),
|
||||
fromId: update.out ? thisId : update.userId,
|
||||
message: update.message,
|
||||
date: update.date,
|
||||
fwdFrom: update.fwdFrom,
|
||||
viaBotId: update.viaBotId,
|
||||
replyToMsgId: update.replyToMsgId,
|
||||
entities: update.entities,
|
||||
}))
|
||||
} else if (update instanceof constructors.UpdateShortChatMessage) {
|
||||
event = new this.Event(new constructors.Message({
|
||||
out: update.out,
|
||||
mentioned: update.mentioned,
|
||||
mediaUnread: update.mediaUnread,
|
||||
silent: update.silent,
|
||||
id: update.id,
|
||||
toId: new constructors.PeerChat(update.chatId),
|
||||
fromId: update.fromId,
|
||||
message: update.message,
|
||||
date: update.date,
|
||||
fwdFrom: update.fwdFrom,
|
||||
viaBotId: update.viaBotId,
|
||||
replyToMsgId: update.replyToMsgId,
|
||||
entities: update.entities,
|
||||
}))
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// Make messages sent to ourselves outgoing unless they're forwarded.
|
||||
// This makes it consistent with official client's appearance.
|
||||
const ori = event.message
|
||||
if (ori.toId instanceof constructors.PeerUser) {
|
||||
if (ori.fromId === ori.toId.userId && !ori.fwdFrom) {
|
||||
event.message.out = true
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
filter(event) {
|
||||
if (this._noCheck) {
|
||||
return event
|
||||
}
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
class Event extends EventCommon {
|
||||
constructor(message) {
|
||||
super()
|
||||
this.message = message
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NewMessage
|
||||
*/
|
||||
@ -1,21 +0,0 @@
|
||||
const { EventBuilder } = require('./common');
|
||||
|
||||
class Raw extends EventBuilder {
|
||||
constructor(args = {
|
||||
types: undefined,
|
||||
func: undefined,
|
||||
}) {
|
||||
super();
|
||||
if (!args.types) {
|
||||
this.types = true;
|
||||
} else {
|
||||
this.types = args.types;
|
||||
}
|
||||
}
|
||||
|
||||
build(update) {
|
||||
return update;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Raw;
|
||||
@ -1,21 +0,0 @@
|
||||
class EventBuilder {
|
||||
constructor(args = {
|
||||
chats: undefined,
|
||||
blacklistChats: undefined,
|
||||
func: undefined,
|
||||
}) {
|
||||
this.chats = args.chats;
|
||||
this.blacklistChats = Boolean(args.blacklistChats);
|
||||
this.resolved = false;
|
||||
this.func = args.func;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
build(update) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EventBuilder,
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
const NewMessage = require('./NewMessage');
|
||||
const Raw = require('./Raw');
|
||||
|
||||
class StopPropagation extends Error {
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NewMessage,
|
||||
StopPropagation,
|
||||
Raw,
|
||||
};
|
||||
@ -1,13 +1,25 @@
|
||||
class AsyncQueue {
|
||||
export default class AsyncQueue<T extends unknown> {
|
||||
private _queue: T[];
|
||||
|
||||
private canGet: Promise<boolean>;
|
||||
|
||||
private resolveGet: (value: boolean) => void;
|
||||
|
||||
private canPush: Promise<boolean> | boolean;
|
||||
|
||||
private resolvePush: (value: boolean) => void;
|
||||
|
||||
constructor() {
|
||||
this._queue = [];
|
||||
this.resolvePush = () => {};
|
||||
this.resolveGet = () => {};
|
||||
this.canGet = new Promise((resolve) => {
|
||||
this.resolveGet = resolve;
|
||||
});
|
||||
this.canPush = true;
|
||||
}
|
||||
|
||||
async push(value) {
|
||||
async push(value: T) {
|
||||
await this.canPush;
|
||||
this._queue.push(value);
|
||||
this.resolveGet(true);
|
||||
@ -26,5 +38,3 @@ class AsyncQueue {
|
||||
return returned;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AsyncQueue;
|
||||
@ -1,14 +1,21 @@
|
||||
const { TypeNotFoundError } = require('../errors/Common');
|
||||
const { coreObjects } = require('../tl/core');
|
||||
const { tlobjects } = require('../tl/AllTLObjects');
|
||||
const { readBigIntFromBuffer } = require('../Helpers');
|
||||
import { TypeNotFoundError } from '../errors';
|
||||
import { coreObjects } from '../tl/core';
|
||||
|
||||
import { readBigIntFromBuffer } from '../Helpers';
|
||||
import { tlobjects } from '../tl/AllTLObjects';
|
||||
|
||||
export default class BinaryReader {
|
||||
private readonly stream: Buffer;
|
||||
|
||||
private _last?: Buffer;
|
||||
|
||||
offset: number;
|
||||
|
||||
class BinaryReader {
|
||||
/**
|
||||
* Small utility class to read binary data.
|
||||
* @param data {Buffer}
|
||||
*/
|
||||
constructor(data) {
|
||||
constructor(data: Buffer) {
|
||||
this.stream = data;
|
||||
this._last = undefined;
|
||||
this.offset = 0;
|
||||
@ -54,8 +61,7 @@ class BinaryReader {
|
||||
* @returns {number}
|
||||
*/
|
||||
readFloat() {
|
||||
return this.read(4)
|
||||
.readFloatLE(0);
|
||||
return this.read(4).readFloatLE(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,8 +70,7 @@ class BinaryReader {
|
||||
*/
|
||||
readDouble() {
|
||||
// was this a bug ? it should have been <d
|
||||
return this.read(8)
|
||||
.readDoubleLE(0);
|
||||
return this.read(8).readDoubleLE(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,7 +78,7 @@ class BinaryReader {
|
||||
* @param bits
|
||||
* @param signed {Boolean}
|
||||
*/
|
||||
readLargeInt(bits, signed = true) {
|
||||
readLargeInt(bits: number, signed = true) {
|
||||
const buffer = this.read(Math.floor(bits / 8));
|
||||
return readBigIntFromBuffer(buffer, true, signed);
|
||||
}
|
||||
@ -81,6 +86,7 @@ class BinaryReader {
|
||||
/**
|
||||
* Read the given amount of bytes, or -1 to read all remaining.
|
||||
* @param length {number}
|
||||
* @param checkLength {boolean} whether to check if the length overflows or not.
|
||||
*/
|
||||
read(length = -1) {
|
||||
if (length === -1) {
|
||||
@ -139,8 +145,7 @@ class BinaryReader {
|
||||
* @returns {string}
|
||||
*/
|
||||
tgReadString() {
|
||||
return this.tgReadBytes()
|
||||
.toString('utf-8');
|
||||
return this.tgReadBytes().toString('utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,7 +161,7 @@ class BinaryReader {
|
||||
// boolFalse
|
||||
return false;
|
||||
} else {
|
||||
throw new Error(`Invalid boolean code ${value.toString('16')}`);
|
||||
throw new Error(`Invalid boolean code ${value.toString(16)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -173,12 +178,13 @@ class BinaryReader {
|
||||
/**
|
||||
* Reads a Telegram object.
|
||||
*/
|
||||
tgReadObject() {
|
||||
tgReadObject(): any {
|
||||
const constructorId = this.readInt(false);
|
||||
|
||||
let clazz = tlobjects[constructorId];
|
||||
if (clazz === undefined) {
|
||||
/**
|
||||
* The class was None, but there's still a
|
||||
* The class was undefined, but there's still a
|
||||
* chance of it being a manually parsed value like bool!
|
||||
*/
|
||||
const value = constructorId;
|
||||
@ -198,7 +204,7 @@ class BinaryReader {
|
||||
return temp;
|
||||
}
|
||||
|
||||
clazz = coreObjects[constructorId];
|
||||
clazz = coreObjects.get(constructorId);
|
||||
|
||||
if (clazz === undefined) {
|
||||
// If there was still no luck, give up
|
||||
@ -230,13 +236,6 @@ class BinaryReader {
|
||||
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* Closes the reader.
|
||||
*/
|
||||
close() {
|
||||
this.stream = undefined;
|
||||
}
|
||||
|
||||
// region Position related
|
||||
|
||||
/**
|
||||
@ -251,7 +250,7 @@ class BinaryReader {
|
||||
* Sets the current position on the stream.
|
||||
* @param position
|
||||
*/
|
||||
setPosition(position) {
|
||||
setPosition(position: number) {
|
||||
this.offset = position;
|
||||
}
|
||||
|
||||
@ -260,11 +259,9 @@ class BinaryReader {
|
||||
* The offset may be negative.
|
||||
* @param offset
|
||||
*/
|
||||
seek(offset) {
|
||||
seek(offset: number) {
|
||||
this.offset += offset;
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
module.exports = BinaryReader;
|
||||
@ -1,15 +0,0 @@
|
||||
class BinaryWriter {
|
||||
constructor(stream) {
|
||||
this._stream = stream;
|
||||
}
|
||||
|
||||
write(buffer) {
|
||||
this._stream = Buffer.concat([this._stream, buffer]);
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this._stream;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BinaryWriter;
|
||||
15
src/lib/gramjs/extensions/BinaryWriter.ts
Normal file
15
src/lib/gramjs/extensions/BinaryWriter.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export default class BinaryWriter {
|
||||
private readonly _buffers: Buffer[];
|
||||
|
||||
constructor(stream: Buffer) {
|
||||
this._buffers = [stream];
|
||||
}
|
||||
|
||||
write(buffer: Buffer) {
|
||||
this._buffers.push(buffer);
|
||||
}
|
||||
|
||||
getValue(): Buffer {
|
||||
return Buffer.concat(this._buffers);
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ AbortSignal.timeout ??= function timeout(ms) {
|
||||
return ctrl.signal;
|
||||
};
|
||||
|
||||
class HttpStream {
|
||||
export default class HttpStream {
|
||||
private url: string | undefined;
|
||||
|
||||
private isClosed: boolean;
|
||||
@ -27,10 +27,23 @@ class HttpStream {
|
||||
this.disconnectedCallback = disconnectedCallback;
|
||||
}
|
||||
|
||||
async readExactly(number: number) {
|
||||
let readData = Buffer.alloc(0);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const thisTime = await this.read();
|
||||
readData = Buffer.concat([readData, thisTime]);
|
||||
number -= thisTime.length;
|
||||
if (number <= 0) {
|
||||
return readData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async read() {
|
||||
await this.canRead;
|
||||
|
||||
const data = this.stream.shift();
|
||||
const data = this.stream.shift()!;
|
||||
if (this.stream.length === 0) {
|
||||
this.canRead = new Promise((resolve, reject) => {
|
||||
this.resolveRead = resolve;
|
||||
@ -41,21 +54,21 @@ class HttpStream {
|
||||
return data;
|
||||
}
|
||||
|
||||
static getURL(ip: string, port: number, testServers: boolean, isPremium: boolean) {
|
||||
static getURL(ip: string, port: number, isTestServer?: boolean, isPremium?: boolean) {
|
||||
if (port === 443) {
|
||||
return `https://${ip}:${port}/apiw1${testServers ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
return `https://${ip}:${port}/apiw1${isTestServer ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
} else {
|
||||
return `http://${ip}:${port}/apiw1${testServers ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
return `http://${ip}:${port}/apiw1${isTestServer ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
async connect(port: number, ip: string, testServers = false, isPremium = false) {
|
||||
async connect(port: number, ip: string, isTestServer = false, isPremium = false) {
|
||||
this.stream = [];
|
||||
this.canRead = new Promise((resolve, reject) => {
|
||||
this.resolveRead = resolve;
|
||||
this.rejectRead = reject;
|
||||
});
|
||||
this.url = HttpStream.getURL(ip, port, testServers, isPremium);
|
||||
this.url = HttpStream.getURL(ip, port, isTestServer, isPremium);
|
||||
|
||||
await fetch(this.url, {
|
||||
method: 'POST',
|
||||
@ -108,5 +121,3 @@ class HttpStream {
|
||||
this.disconnectedCallback = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default HttpStream;
|
||||
|
||||
@ -1,15 +1,23 @@
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let _level;
|
||||
export type LoggerLevel = 'error' | 'warn' | 'info' | 'debug';
|
||||
|
||||
class Logger {
|
||||
static LEVEL_MAP = new Map([
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let _level: LoggerLevel;
|
||||
|
||||
type ColorKey = LoggerLevel | 'start' | 'end';
|
||||
|
||||
export default class Logger {
|
||||
static LEVEL_MAP = new Map<LoggerLevel, Set<LoggerLevel>>([
|
||||
['error', new Set(['error'])],
|
||||
['warn', new Set(['error', 'warn'])],
|
||||
['info', new Set(['error', 'warn', 'info'])],
|
||||
['debug', new Set(['error', 'warn', 'info', 'debug'])],
|
||||
]);
|
||||
|
||||
constructor(level) {
|
||||
colors: Record<ColorKey, string>;
|
||||
|
||||
messageFormat: string;
|
||||
|
||||
constructor(level?: LoggerLevel) {
|
||||
if (!_level) {
|
||||
_level = level || 'debug';
|
||||
}
|
||||
@ -25,59 +33,38 @@ class Logger {
|
||||
this.messageFormat = '[%t] [%l] - [%m]';
|
||||
}
|
||||
|
||||
static setLevel(level) {
|
||||
static setLevel(level: LoggerLevel) {
|
||||
_level = level;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param level {string}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
canSend(level) {
|
||||
return Logger.LEVEL_MAP.get(_level).has(level);
|
||||
canSend(level: LoggerLevel) {
|
||||
if (!_level) return false;
|
||||
return Logger.LEVEL_MAP.get(_level)!.has(level);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message {string}
|
||||
*/
|
||||
warn(message) {
|
||||
warn(message: string) {
|
||||
this._log('warn', message, this.colors.warn);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message {string}
|
||||
*/
|
||||
info(message) {
|
||||
info(message: string) {
|
||||
this._log('info', message, this.colors.info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message {string}
|
||||
*/
|
||||
debug(message) {
|
||||
debug(message: string) {
|
||||
this._log('debug', message, this.colors.debug);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param message {string}
|
||||
*/
|
||||
error(message) {
|
||||
error(message: string) {
|
||||
this._log('error', message, this.colors.error);
|
||||
}
|
||||
|
||||
format(message, level) {
|
||||
format(message: string, level: LoggerLevel) {
|
||||
return this.messageFormat.replace('%t', new Date().toISOString())
|
||||
.replace('%l', level.toUpperCase())
|
||||
.replace('%m', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param level {string}
|
||||
* @param message {string}
|
||||
* @param color {string}
|
||||
*/
|
||||
_log(level, message, color) {
|
||||
_log(level: LoggerLevel, message: string, color: string) {
|
||||
if (!_level) {
|
||||
return;
|
||||
}
|
||||
@ -87,5 +74,3 @@ class Logger {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Logger;
|
||||
@ -1,14 +1,31 @@
|
||||
const MessageContainer = require('../tl/core/MessageContainer');
|
||||
const TLMessage = require('../tl/core/TLMessage');
|
||||
const BinaryWriter = require('./BinaryWriter');
|
||||
import type MTProtoState from '../network/MTProtoState';
|
||||
import type RequestState from '../network/RequestState';
|
||||
import type Logger from './Logger';
|
||||
|
||||
import TLMessage from '../tl/core/TLMessage';
|
||||
|
||||
import MessageContainer from '../tl/core/MessageContainer';
|
||||
import BinaryWriter from './BinaryWriter';
|
||||
|
||||
const USE_INVOKE_AFTER_WITH = new Set([
|
||||
'messages.SendMessage', 'messages.SendMedia', 'messages.SendMultiMedia',
|
||||
'messages.ForwardMessages', 'messages.SendInlineBotResult',
|
||||
]);
|
||||
|
||||
class MessagePacker {
|
||||
constructor(state, logger) {
|
||||
export default class MessagePacker {
|
||||
private _state: MTProtoState;
|
||||
|
||||
public _pendingStates: RequestState[];
|
||||
|
||||
private _queue: (RequestState | undefined)[];
|
||||
|
||||
private _ready: Promise<unknown>;
|
||||
|
||||
setReady: ((value?: any) => void) | undefined;
|
||||
|
||||
private _log: Logger;
|
||||
|
||||
constructor(state: MTProtoState, logger: Logger) {
|
||||
this._state = state;
|
||||
this._queue = [];
|
||||
this._pendingStates = [];
|
||||
@ -27,14 +44,14 @@ class MessagePacker {
|
||||
this.append(undefined);
|
||||
}
|
||||
|
||||
append(state, setReady = true, atStart = false) {
|
||||
append(state?: RequestState, setReady = true, atStart = false) {
|
||||
// We need to check if there is already a `USE_INVOKE_AFTER_WITH` request
|
||||
if (state && USE_INVOKE_AFTER_WITH.has(state.request.className)) {
|
||||
if (atStart) {
|
||||
// Assign `after` for the previously first `USE_INVOKE_AFTER_WITH` request
|
||||
for (let i = 0; i < this._queue.length; i++) {
|
||||
if (USE_INVOKE_AFTER_WITH.has(this._queue[i]?.request.className)) {
|
||||
this._queue[i].after = state;
|
||||
this._queue[i]!.after = state;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -56,7 +73,7 @@ class MessagePacker {
|
||||
}
|
||||
|
||||
if (setReady) {
|
||||
this.setReady(true);
|
||||
this.setReady?.(true);
|
||||
}
|
||||
|
||||
// 1658238041=MsgsAck, we don't care about MsgsAck here because they never resolve anyway.
|
||||
@ -64,7 +81,7 @@ class MessagePacker {
|
||||
this._pendingStates.push(state);
|
||||
state.promise
|
||||
// Using finally causes triggering `unhandledrejection` event
|
||||
.catch(() => {
|
||||
?.catch(() => {
|
||||
})
|
||||
.finally(() => {
|
||||
this._pendingStates = this._pendingStates.filter((s) => s !== state);
|
||||
@ -72,22 +89,22 @@ class MessagePacker {
|
||||
}
|
||||
}
|
||||
|
||||
prepend(states) {
|
||||
prepend(states: RequestState[]) {
|
||||
states.reverse().forEach((state) => {
|
||||
this.append(state, false, true);
|
||||
});
|
||||
|
||||
this.setReady(true);
|
||||
this.setReady?.(true);
|
||||
}
|
||||
|
||||
extend(states) {
|
||||
extend(states: RequestState[]) {
|
||||
states.forEach((state) => {
|
||||
this.append(state, false);
|
||||
});
|
||||
this.setReady(true);
|
||||
this.setReady?.(true);
|
||||
}
|
||||
|
||||
async getBeacon(state) {
|
||||
async getBeacon(state: RequestState) {
|
||||
const buffer = new BinaryWriter(Buffer.alloc(0));
|
||||
const size = state.data.length + TLMessage.SIZE_OVERHEAD;
|
||||
if (size <= MessageContainer.MAXIMUM_SIZE) {
|
||||
@ -105,7 +122,7 @@ class MessagePacker {
|
||||
}
|
||||
this._log.warn(`Message payload for ${state.request.className
|
||||
|| state.request.constructor.name} is too long ${state.data.length} and cannot be sent`);
|
||||
state.reject('Request Payload is too big');
|
||||
state.reject?.(new Error('Request Payload is too big'));
|
||||
|
||||
return undefined;
|
||||
}
|
||||
@ -137,7 +154,7 @@ class MessagePacker {
|
||||
}
|
||||
|
||||
if (state.abortSignal?.aborted) {
|
||||
state.reject(new Error('Request aborted'));
|
||||
state.reject?.(new Error('Request aborted'));
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -163,7 +180,7 @@ class MessagePacker {
|
||||
|
||||
this._log.warn(`Message payload for ${state.request.className
|
||||
|| state.request.constructor.name} is too long ${state.data.length} and cannot be sent`);
|
||||
state.reject('Request Payload is too big');
|
||||
state.reject?.(new Error('Request Payload is too big'));
|
||||
size = 0;
|
||||
}
|
||||
if (!batch.length) {
|
||||
@ -190,5 +207,3 @@ class MessagePacker {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessagePacker;
|
||||
@ -1,17 +1,23 @@
|
||||
class PendingState {
|
||||
import type BigInt from 'big-integer';
|
||||
|
||||
import type RequestState from '../network/RequestState';
|
||||
|
||||
export default class PendingState {
|
||||
_pending: Map<string, RequestState>;
|
||||
|
||||
constructor() {
|
||||
this._pending = new Map();
|
||||
}
|
||||
|
||||
set(msgId, state) {
|
||||
set(msgId: BigInt.BigInteger, state: RequestState) {
|
||||
this._pending.set(msgId.toString(), state);
|
||||
}
|
||||
|
||||
get(msgId) {
|
||||
get(msgId: BigInt.BigInteger) {
|
||||
return this._pending.get(msgId.toString());
|
||||
}
|
||||
|
||||
getAndDelete(msgId) {
|
||||
getAndDelete(msgId: BigInt.BigInteger) {
|
||||
const state = this.get(msgId);
|
||||
this.delete(msgId);
|
||||
return state;
|
||||
@ -21,7 +27,7 @@ class PendingState {
|
||||
return Array.from(this._pending.values());
|
||||
}
|
||||
|
||||
delete(msgId) {
|
||||
delete(msgId: BigInt.BigInteger) {
|
||||
this._pending.delete(msgId.toString());
|
||||
}
|
||||
|
||||
@ -29,5 +35,3 @@ class PendingState {
|
||||
this._pending.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PendingState;
|
||||
@ -1,4 +1,4 @@
|
||||
const { Mutex } = require('async-mutex');
|
||||
import { Mutex } from "async-mutex";
|
||||
|
||||
const mutex = new Mutex();
|
||||
|
||||
@ -6,22 +6,32 @@ const closeError = new Error('WebSocket was closed');
|
||||
const CONNECTION_TIMEOUT = 3000;
|
||||
const MAX_TIMEOUT = 30000;
|
||||
|
||||
class PromisedWebSockets {
|
||||
constructor(disconnectedCallback) {
|
||||
/* CONTEST
|
||||
this.isBrowser = typeof process === 'undefined' ||
|
||||
process.type === 'renderer' ||
|
||||
process.browser === true ||
|
||||
process.__nwjs
|
||||
export default class PromisedWebSockets {
|
||||
private closed: boolean;
|
||||
|
||||
*/
|
||||
private timeout: number;
|
||||
|
||||
private stream: Buffer;
|
||||
|
||||
private canRead?: boolean | Promise<boolean>;
|
||||
|
||||
private resolveRead: ((value?: any) => void) | undefined;
|
||||
|
||||
private client: WebSocket | undefined;
|
||||
|
||||
private website?: string;
|
||||
|
||||
private disconnectedCallback: () => void;
|
||||
|
||||
constructor(disconnectedCallback: () => void) {
|
||||
this.client = undefined;
|
||||
this.closed = true;
|
||||
this.stream = Buffer.alloc(0);
|
||||
this.disconnectedCallback = disconnectedCallback;
|
||||
this.timeout = CONNECTION_TIMEOUT;
|
||||
}
|
||||
|
||||
async readExactly(number) {
|
||||
async readExactly(number: number) {
|
||||
let readData = Buffer.alloc(0);
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
@ -34,7 +44,7 @@ class PromisedWebSockets {
|
||||
}
|
||||
}
|
||||
|
||||
async read(number) {
|
||||
async read(number: number) {
|
||||
if (this.closed) {
|
||||
throw closeError;
|
||||
}
|
||||
@ -66,25 +76,26 @@ class PromisedWebSockets {
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
getWebSocketLink(ip, port, testServers, isPremium) {
|
||||
getWebSocketLink(ip: string, port: number, isTestServer?: boolean, isPremium?: boolean) {
|
||||
if (port === 443) {
|
||||
return `wss://${ip}:${port}/apiws${testServers ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
return `wss://${ip}:${port}/apiws${isTestServer ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
} else {
|
||||
return `ws://${ip}:${port}/apiws${testServers ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
return `ws://${ip}:${port}/apiws${isTestServer ? '_test' : ''}${isPremium ? '_premium' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
connect(port, ip, testServers = false, isPremium = false) {
|
||||
connect(port: number, ip: string, isTestServer = false, isPremium = false) {
|
||||
this.stream = Buffer.alloc(0);
|
||||
this.canRead = new Promise((resolve) => {
|
||||
this.resolveRead = resolve;
|
||||
});
|
||||
this.closed = false;
|
||||
this.website = this.getWebSocketLink(ip, port, testServers, isPremium);
|
||||
this.website = this.getWebSocketLink(ip, port, isTestServer, isPremium);
|
||||
this.client = new WebSocket(this.website, 'binary');
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.client) return;
|
||||
let hasResolved = false;
|
||||
let timeout;
|
||||
let timeout: ReturnType<typeof globalThis.setTimeout> | undefined;
|
||||
this.client.onopen = () => {
|
||||
this.receive();
|
||||
resolve(this);
|
||||
@ -105,7 +116,7 @@ class PromisedWebSockets {
|
||||
console.error(`Socket ${ip} closed. Code: ${code}, reason: ${reason}, was clean: ${wasClean}`);
|
||||
}
|
||||
|
||||
this.resolveRead(false);
|
||||
this.resolveRead?.(false);
|
||||
this.closed = true;
|
||||
if (this.disconnectedCallback) {
|
||||
this.disconnectedCallback();
|
||||
@ -118,12 +129,12 @@ class PromisedWebSockets {
|
||||
if (hasResolved) return;
|
||||
|
||||
reject(new Error('WebSocket connection timeout'));
|
||||
this.resolveRead(false);
|
||||
this.resolveRead?.(false);
|
||||
this.closed = true;
|
||||
if (this.disconnectedCallback) {
|
||||
this.disconnectedCallback();
|
||||
}
|
||||
this.client.close();
|
||||
this.client?.close();
|
||||
this.timeout *= 2;
|
||||
this.timeout = Math.min(this.timeout, MAX_TIMEOUT);
|
||||
timeout = undefined;
|
||||
@ -134,34 +145,33 @@ class PromisedWebSockets {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
self.addEventListener('offline', async () => {
|
||||
await this.close();
|
||||
this.resolveRead(false);
|
||||
this.resolveRead?.(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
write(data) {
|
||||
write(data: Buffer) {
|
||||
if (this.closed) {
|
||||
throw closeError;
|
||||
}
|
||||
this.client.send(data);
|
||||
this.client?.send(data);
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.client.close();
|
||||
await this.client?.close();
|
||||
this.closed = true;
|
||||
}
|
||||
|
||||
receive() {
|
||||
if (!this.client) return;
|
||||
this.client.onmessage = async (message) => {
|
||||
await mutex.runExclusive(async () => {
|
||||
const data = message.data instanceof ArrayBuffer
|
||||
? Buffer.from(message.data)
|
||||
: Buffer.from(await new Response(message.data).arrayBuffer());
|
||||
this.stream = Buffer.concat([this.stream, data]);
|
||||
this.resolveRead(true);
|
||||
this.resolveRead?.(true);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PromisedWebSockets;
|
||||
@ -1,15 +0,0 @@
|
||||
const Logger = require('./Logger');
|
||||
const BinaryWriter = require('./BinaryWriter');
|
||||
const BinaryReader = require('./BinaryReader');
|
||||
const PromisedWebSockets = require('./PromisedWebSockets');
|
||||
const MessagePacker = require('./MessagePacker');
|
||||
const AsyncQueue = require('./AsyncQueue');
|
||||
|
||||
module.exports = {
|
||||
BinaryWriter,
|
||||
BinaryReader,
|
||||
MessagePacker,
|
||||
AsyncQueue,
|
||||
Logger,
|
||||
PromisedWebSockets,
|
||||
};
|
||||
19
src/lib/gramjs/extensions/index.ts
Normal file
19
src/lib/gramjs/extensions/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import AsyncQueue from './AsyncQueue';
|
||||
import BinaryReader from './BinaryReader';
|
||||
import BinaryWriter from './BinaryWriter';
|
||||
import HttpStream from './HttpStream';
|
||||
import Logger from './Logger';
|
||||
import MessagePacker from './MessagePacker';
|
||||
import PendingState from './PendingState';
|
||||
import PromisedWebSockets from './PromisedWebSockets';
|
||||
|
||||
export {
|
||||
AsyncQueue,
|
||||
BinaryReader,
|
||||
BinaryWriter,
|
||||
HttpStream,
|
||||
Logger,
|
||||
MessagePacker,
|
||||
PendingState,
|
||||
PromisedWebSockets,
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
const Api = require('./tl/api');
|
||||
const TelegramClient = require('./client/TelegramClient');
|
||||
const connection = require('./network');
|
||||
const tl = require('./tl');
|
||||
const version = require('./Version');
|
||||
const events = require('./events');
|
||||
const utils = require('./Utils');
|
||||
const errors = require('./errors');
|
||||
const sessions = require('./sessions');
|
||||
const extensions = require('./extensions');
|
||||
const helpers = require('./Helpers');
|
||||
|
||||
module.exports = {
|
||||
Api,
|
||||
TelegramClient,
|
||||
sessions,
|
||||
connection,
|
||||
extensions,
|
||||
tl,
|
||||
version,
|
||||
events,
|
||||
utils,
|
||||
errors,
|
||||
helpers,
|
||||
};
|
||||
19
src/lib/gramjs/index.ts
Normal file
19
src/lib/gramjs/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export { Api } from './tl';
|
||||
export * as errors from './errors';
|
||||
export * as extensions from './extensions';
|
||||
export * as connection from './network';
|
||||
export * as sessions from './sessions';
|
||||
export * as tl from './tl';
|
||||
|
||||
import TelegramClient, { Update, SizeType } from './client/TelegramClient';
|
||||
export * as helpers from './Helpers';
|
||||
export * as utils from './Utils';
|
||||
|
||||
export {
|
||||
TelegramClient,
|
||||
};
|
||||
|
||||
export type {
|
||||
Update,
|
||||
SizeType,
|
||||
}
|
||||
@ -4,27 +4,39 @@
|
||||
* @param log
|
||||
* @returns {Promise<{authKey: *, timeOffset: *}>}
|
||||
*/
|
||||
// eslint-disable-next-line import/no-named-default
|
||||
import { default as Api } from '../tl/api';
|
||||
import { SecurityError } from '../errors';
|
||||
// eslint-disable-next-line import/no-named-default
|
||||
import type { default as MTProtoPlainSender } from './MTProtoPlainSender';
|
||||
import { SERVER_KEYS } from '../crypto/RSA';
|
||||
|
||||
const bigInt = require('big-integer');
|
||||
const IGE = require('../crypto/IGE');
|
||||
const AuthKey = require('../crypto/AuthKey');
|
||||
const Factorizator = require('../crypto/Factorizator');
|
||||
const Helpers = require('../Helpers');
|
||||
const BinaryReader = require('../extensions/BinaryReader');
|
||||
import bigInt from 'big-integer';
|
||||
|
||||
import type MTProtoPlainSender from './MTProtoPlainSender';
|
||||
|
||||
import { IGE } from '../crypto/IGE';
|
||||
import { SERVER_KEYS } from '../crypto/RSA';
|
||||
import { SecurityError } from '../errors';
|
||||
import { BinaryReader } from '../extensions';
|
||||
import { Api } from '../tl';
|
||||
|
||||
import { AuthKey } from '../crypto/AuthKey';
|
||||
import { Factorizator } from '../crypto/Factorizator';
|
||||
import {
|
||||
bufferXor,
|
||||
generateKeyDataFromNonce,
|
||||
generateRandomBytes,
|
||||
getByteArray,
|
||||
modExp,
|
||||
readBigIntFromBuffer,
|
||||
readBufferFromBigInt,
|
||||
sha1,
|
||||
sha256,
|
||||
toSignedLittleBuffer,
|
||||
} from '../Helpers';
|
||||
|
||||
const RETRIES = 20;
|
||||
|
||||
export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
// Step 1 sending: PQ Request, endianness doesn't matter since it's random
|
||||
let bytes = Helpers.generateRandomBytes(16);
|
||||
let bytes = generateRandomBytes(16);
|
||||
|
||||
const nonce = Helpers.readBigIntFromBuffer(bytes, false, true);
|
||||
const nonce = readBigIntFromBuffer(bytes, false, true);
|
||||
const resPQ = await sender.send(new Api.ReqPqMulti({ nonce }));
|
||||
log.debug('Starting authKey generation step 1');
|
||||
|
||||
@ -34,18 +46,18 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
if (resPQ.nonce.neq(nonce)) {
|
||||
throw new SecurityError('Step 1 invalid nonce from server');
|
||||
}
|
||||
const pq = Helpers.readBigIntFromBuffer(resPQ.pq, false, true);
|
||||
const pq = readBigIntFromBuffer(resPQ.pq, false, true);
|
||||
log.debug('Finished authKey generation step 1');
|
||||
// Step 2 sending: DH Exchange
|
||||
const { p, q } = Factorizator.factorize(pq);
|
||||
|
||||
const pBuffer = Helpers.getByteArray(p);
|
||||
const qBuffer = Helpers.getByteArray(q);
|
||||
const pBuffer = getByteArray(p);
|
||||
const qBuffer = getByteArray(q);
|
||||
|
||||
bytes = Helpers.generateRandomBytes(32);
|
||||
const newNonce = Helpers.readBigIntFromBuffer(bytes, true, true);
|
||||
bytes = generateRandomBytes(32);
|
||||
const newNonce = readBigIntFromBuffer(bytes, true, true);
|
||||
const pqInnerData = new Api.PQInnerData({
|
||||
pq: Helpers.getByteArray(pq), // unsigned
|
||||
pq: getByteArray(pq), // unsigned
|
||||
p: pBuffer,
|
||||
q: qBuffer,
|
||||
nonce: resPQ.nonce,
|
||||
@ -70,28 +82,28 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
);
|
||||
}
|
||||
// Value should be padded to be made 192 exactly
|
||||
const padding = Helpers.generateRandomBytes(192 - pqInnerData.length);
|
||||
const padding = generateRandomBytes(192 - pqInnerData.length);
|
||||
const dataWithPadding = Buffer.concat([pqInnerData, padding]);
|
||||
const dataPadReversed = Buffer.from(dataWithPadding).reverse();
|
||||
|
||||
let encryptedData;
|
||||
for (let i = 0; i < RETRIES; i++) {
|
||||
const tempKey = Helpers.generateRandomBytes(32);
|
||||
const shaDigestKeyWithData = await Helpers.sha256(Buffer.concat([tempKey, dataWithPadding]));
|
||||
const tempKey = generateRandomBytes(32);
|
||||
const shaDigestKeyWithData = await sha256(Buffer.concat([tempKey, dataWithPadding]));
|
||||
const dataWithHash = Buffer.concat([dataPadReversed, shaDigestKeyWithData]);
|
||||
|
||||
const ige = new IGE(tempKey, Buffer.alloc(32));
|
||||
const aesEncrypted = ige.encryptIge(dataWithHash);
|
||||
const tempKeyXor = Helpers.bufferXor(tempKey, await Helpers.sha256(aesEncrypted));
|
||||
const tempKeyXor = bufferXor(tempKey, await sha256(aesEncrypted));
|
||||
|
||||
const keyAesEncrypted = Buffer.concat([tempKeyXor, aesEncrypted]);
|
||||
const keyAesEncryptedInt = Helpers.readBigIntFromBuffer(keyAesEncrypted, false, false);
|
||||
const keyAesEncryptedInt = readBigIntFromBuffer(keyAesEncrypted, false, false);
|
||||
if (keyAesEncryptedInt.greaterOrEquals(targetKey.n)) {
|
||||
log.debug('Aes key greater than RSA. retrying');
|
||||
continue;
|
||||
}
|
||||
const encryptedDataBuffer = Helpers.modExp(keyAesEncryptedInt, bigInt(targetKey.e), targetKey.n);
|
||||
encryptedData = Helpers.readBufferFromBigInt(encryptedDataBuffer, 256, false, false);
|
||||
const encryptedDataBuffer = modExp(keyAesEncryptedInt, bigInt(targetKey.e), targetKey.n);
|
||||
encryptedData = readBufferFromBigInt(encryptedDataBuffer, 256, false, false);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -130,10 +142,10 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
}
|
||||
|
||||
if (serverDhParams instanceof Api.ServerDHParamsFail) {
|
||||
const sh = await Helpers.sha1(
|
||||
Helpers.toSignedLittleBuffer(newNonce, 32).slice(4, 20),
|
||||
const sh = await sha1(
|
||||
toSignedLittleBuffer(newNonce, 32).slice(4, 20),
|
||||
);
|
||||
const nnh = Helpers.readBigIntFromBuffer(sh, true, true);
|
||||
const nnh = readBigIntFromBuffer(sh, true, true);
|
||||
if (serverDhParams.newNonceHash.neq(nnh)) {
|
||||
throw new SecurityError('Step 2 invalid DH fail nonce from server');
|
||||
}
|
||||
@ -145,7 +157,7 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
log.debug('Starting authKey generation step 3');
|
||||
|
||||
// Step 3 sending: Complete DH Exchange
|
||||
const { key, iv } = await Helpers.generateKeyDataFromNonce(
|
||||
const { key, iv } = await generateKeyDataFromNonce(
|
||||
resPQ.serverNonce,
|
||||
newNonce,
|
||||
);
|
||||
@ -161,7 +173,7 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
if (!(serverDhInner instanceof Api.ServerDHInnerData)) {
|
||||
throw new Error(`Step 3 answer was ${serverDhInner}`);
|
||||
}
|
||||
const sha1Answer = await Helpers.sha1(serverDhInner.getBytes());
|
||||
const sha1Answer = await sha1(serverDhInner.getBytes());
|
||||
if (!(hash.equals(sha1Answer))) {
|
||||
throw new SecurityError('Step 3 Invalid hash answer');
|
||||
}
|
||||
@ -183,20 +195,20 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
throw new SecurityError('Step 3 invalid dhPrime or g');
|
||||
}
|
||||
|
||||
const dhPrime = Helpers.readBigIntFromBuffer(
|
||||
const dhPrime = readBigIntFromBuffer(
|
||||
serverDhInner.dhPrime,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
const ga = Helpers.readBigIntFromBuffer(serverDhInner.gA, false, false);
|
||||
const ga = readBigIntFromBuffer(serverDhInner.gA, false, false);
|
||||
const timeOffset = serverDhInner.serverTime - Math.floor(Date.now() / 1000);
|
||||
const b = Helpers.readBigIntFromBuffer(
|
||||
Helpers.generateRandomBytes(256),
|
||||
const b = readBigIntFromBuffer(
|
||||
generateRandomBytes(256),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
const gb = Helpers.modExp(bigInt(serverDhInner.g), b, dhPrime);
|
||||
const gab = Helpers.modExp(ga, b, dhPrime);
|
||||
const gb = modExp(bigInt(serverDhInner.g), b, dhPrime);
|
||||
const gab = modExp(ga, b, dhPrime);
|
||||
|
||||
if (ga.lesserOrEquals(1)) {
|
||||
throw new SecurityError('Step 3 failed ga > 1 check');
|
||||
@ -223,11 +235,11 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
nonce: resPQ.nonce,
|
||||
serverNonce: resPQ.serverNonce,
|
||||
retryId: bigInt.zero, // TODO Actual retry ID
|
||||
gB: Helpers.getByteArray(gb, false),
|
||||
gB: getByteArray(gb, false),
|
||||
}).getBytes();
|
||||
|
||||
const clientDdhInnerHashed = Buffer.concat([
|
||||
await Helpers.sha1(clientDhInner),
|
||||
await sha1(clientDhInner),
|
||||
clientDhInner,
|
||||
]);
|
||||
// Encryption
|
||||
@ -262,7 +274,7 @@ export async function doAuthentication(sender: MTProtoPlainSender, log: any) {
|
||||
);
|
||||
}
|
||||
const authKey = new AuthKey();
|
||||
await authKey.setKey(Helpers.getByteArray(gab));
|
||||
await authKey.setKey(getByteArray(gab));
|
||||
|
||||
const nonceNumber = 1 + nonceTypesString.indexOf(dhGen.className);
|
||||
|
||||
|
||||
@ -2,24 +2,34 @@
|
||||
* This module contains the class used to communicate with Telegram's servers
|
||||
* in plain text, when no authorization key has been created yet.
|
||||
*/
|
||||
const BigInt = require('big-integer');
|
||||
const MTProtoState = require('./MTProtoState');
|
||||
const BinaryReader = require('../extensions/BinaryReader');
|
||||
const { InvalidBufferError } = require('../errors/Common');
|
||||
const { toSignedLittleBuffer } = require('../Helpers');
|
||||
import BigInt from 'big-integer';
|
||||
|
||||
import type { Logger } from '../extensions';
|
||||
import type { Api } from '../tl';
|
||||
import type { Connection } from './connection';
|
||||
|
||||
import { BinaryReader } from '../extensions';
|
||||
|
||||
import { InvalidBufferError } from '../errors/Common';
|
||||
import { toSignedLittleBuffer } from '../Helpers';
|
||||
import MTProtoState from './MTProtoState';
|
||||
|
||||
/**
|
||||
* MTProto Mobile Protocol plain sender (https://core.telegram.org/mtproto/description#unencrypted-messages)
|
||||
*/
|
||||
|
||||
class MTProtoPlainSender {
|
||||
export default class MTProtoPlainSender {
|
||||
private _state: MTProtoState;
|
||||
|
||||
private _connection: Connection;
|
||||
|
||||
/**
|
||||
* Initializes the MTProto plain sender.
|
||||
* @param connection connection: the Connection to be used.
|
||||
* @param loggers
|
||||
*/
|
||||
constructor(connection, loggers) {
|
||||
this._state = new MTProtoState(connection, loggers);
|
||||
constructor(connection: Connection, loggers: Logger) {
|
||||
this._state = new MTProtoState(undefined, loggers);
|
||||
this._connection = connection;
|
||||
}
|
||||
|
||||
@ -27,7 +37,7 @@ class MTProtoPlainSender {
|
||||
* Sends and receives the result for the given request.
|
||||
* @param request
|
||||
*/
|
||||
async send(request) {
|
||||
async send(request: Api.AnyRequest) {
|
||||
let body = request.getBytes();
|
||||
let msgId = this._state._getNewMsgId();
|
||||
const m = toSignedLittleBuffer(msgId, 8);
|
||||
@ -68,5 +78,3 @@ class MTProtoPlainSender {
|
||||
return reader.tgReadObject();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MTProtoPlainSender;
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,47 @@
|
||||
const BigInt = require('big-integer');
|
||||
const aes = require('@cryptography/aes');
|
||||
import BigInt from 'big-integer';
|
||||
|
||||
const Helpers = require('../Helpers');
|
||||
const IGE = require('../crypto/IGE');
|
||||
const BinaryReader = require('../extensions/BinaryReader');
|
||||
const GZIPPacked = require('../tl/core/GZIPPacked');
|
||||
const { TLMessage } = require('../tl/core');
|
||||
const {
|
||||
SecurityError,
|
||||
InvalidBufferError,
|
||||
} = require('../errors/Common');
|
||||
const { InvokeAfterMsg } = require('../tl').requests;
|
||||
const {
|
||||
import type { AuthKey } from '../crypto/AuthKey';
|
||||
|
||||
import { IGE } from '../crypto/IGE';
|
||||
import { BinaryReader, type BinaryWriter, type Logger } from '../extensions';
|
||||
import { Api } from '../tl';
|
||||
import { TLMessage } from '../tl/core';
|
||||
import GZIPPacked from '../tl/core/GZIPPacked';
|
||||
|
||||
import { InvalidBufferError, SecurityError } from '../errors/Common';
|
||||
import {
|
||||
convertToLittle,
|
||||
generateRandomBytes,
|
||||
generateRandomLong,
|
||||
mod,
|
||||
readBigIntFromBuffer,
|
||||
readBufferFromBigInt,
|
||||
sha256,
|
||||
toSignedLittleBuffer,
|
||||
} = require('../Helpers');
|
||||
} from '../Helpers';
|
||||
import { CTR } from '../crypto/CTR';
|
||||
|
||||
export default class MTProtoState {
|
||||
private readonly authKey?: AuthKey;
|
||||
|
||||
private _log: any;
|
||||
|
||||
timeOffset: number;
|
||||
|
||||
salt: bigInt.BigInteger;
|
||||
|
||||
private id: bigInt.BigInteger;
|
||||
|
||||
_sequence: number;
|
||||
|
||||
_isCall: boolean;
|
||||
|
||||
_isOutgoing: boolean;
|
||||
|
||||
private _lastMsgId: bigInt.BigInteger;
|
||||
|
||||
private msgIds: string[];
|
||||
|
||||
class MTProtoState {
|
||||
/**
|
||||
*
|
||||
`telethon.network.mtprotosender.MTProtoSender` needs to hold a state
|
||||
@ -43,17 +69,17 @@ class MTProtoState {
|
||||
* @param isCall
|
||||
* @param isOutgoing
|
||||
*/
|
||||
constructor(authKey, loggers, isCall = false, isOutgoing = false) {
|
||||
constructor(authKey?: AuthKey, loggers?: Logger, isCall = false, isOutgoing = false) {
|
||||
this.authKey = authKey;
|
||||
this._log = loggers;
|
||||
this._isCall = isCall;
|
||||
this._isOutgoing = isOutgoing;
|
||||
this.timeOffset = 0;
|
||||
this.salt = 0;
|
||||
this.salt = BigInt.zero;
|
||||
|
||||
this.id = undefined;
|
||||
this._sequence = undefined;
|
||||
this._lastMsgId = undefined;
|
||||
this.id = BigInt.zero;
|
||||
this._sequence = 0;
|
||||
this._lastMsgId = BigInt.zero;
|
||||
this.msgIds = [];
|
||||
this.reset();
|
||||
}
|
||||
@ -63,7 +89,7 @@ class MTProtoState {
|
||||
*/
|
||||
reset() {
|
||||
// Session IDs can be random on every connection
|
||||
this.id = Helpers.generateRandomLong(true);
|
||||
this.id = generateRandomLong(true);
|
||||
this._sequence = 0;
|
||||
this._lastMsgId = BigInt(0);
|
||||
this.msgIds = [];
|
||||
@ -74,7 +100,7 @@ class MTProtoState {
|
||||
* used when the time offset changed.
|
||||
* @param message
|
||||
*/
|
||||
updateMessageId(message) {
|
||||
updateMessageId(message: TLMessage) {
|
||||
message.msgId = this._getNewMsgId();
|
||||
}
|
||||
|
||||
@ -85,11 +111,13 @@ class MTProtoState {
|
||||
* @param client
|
||||
* @returns {{iv: Buffer, key: Buffer}}
|
||||
*/
|
||||
async _calcKey(authKey, msgKey, client) {
|
||||
const x = (this._isCall ? 128 + ((this._isOutgoing ^ client) ? 8 : 0) : (client === true ? 0 : 8));
|
||||
async _calcKey(authKey: Buffer, msgKey: Buffer, client: boolean) {
|
||||
const x = this._isCall
|
||||
? (128 + (this._isOutgoing !== client ? 8 : 0))
|
||||
: (client ? 0 : 8);
|
||||
const [sha256a, sha256b] = await Promise.all([
|
||||
Helpers.sha256(Buffer.concat([msgKey, authKey.slice(x, x + 36)])),
|
||||
Helpers.sha256(Buffer.concat([authKey.slice(x + 40, x + 76), msgKey])),
|
||||
sha256(Buffer.concat([msgKey, authKey.slice(x, x + 36)])),
|
||||
sha256(Buffer.concat([authKey.slice(x + 40, x + 76), msgKey])),
|
||||
]);
|
||||
const key = Buffer.concat([sha256a.slice(0, 8), sha256b.slice(8, 24), sha256a.slice(24, 32)]);
|
||||
if (this._isCall) {
|
||||
@ -115,7 +143,7 @@ class MTProtoState {
|
||||
* @param contentRelated
|
||||
* @param afterId
|
||||
*/
|
||||
async writeDataAsMessage(buffer, data, contentRelated, afterId) {
|
||||
async writeDataAsMessage(buffer: BinaryWriter, data: Buffer, contentRelated: boolean, afterId?: BigInt.BigInteger) {
|
||||
const msgId = this._getNewMsgId();
|
||||
const seqNo = this._getSeqNo(contentRelated);
|
||||
let body;
|
||||
@ -123,7 +151,7 @@ class MTProtoState {
|
||||
body = await GZIPPacked.gzipIfSmaller(contentRelated, data);
|
||||
} else {
|
||||
// Invoke query expects a query with a getBytes func
|
||||
body = await GZIPPacked.gzipIfSmaller(contentRelated, new InvokeAfterMsg({
|
||||
body = await GZIPPacked.gzipIfSmaller(contentRelated, new Api.InvokeAfterMsg({
|
||||
msgId: afterId,
|
||||
query: {
|
||||
getBytes() {
|
||||
@ -147,8 +175,21 @@ class MTProtoState {
|
||||
* following MTProto 2.0 guidelines core.telegram.org/mtproto/description.
|
||||
* @param data
|
||||
*/
|
||||
async encryptMessageData(data) {
|
||||
async encryptMessageData(data: Buffer) {
|
||||
if (!this.authKey) {
|
||||
throw new Error('Auth key unset');
|
||||
}
|
||||
|
||||
await this.authKey.waitForKey();
|
||||
const authKey = this.authKey.getKey();
|
||||
if (!authKey) {
|
||||
throw new Error('Auth key unset');
|
||||
}
|
||||
|
||||
if (!this.salt || !this.id || !authKey || !this.authKey.keyId) {
|
||||
throw new Error('Unset params');
|
||||
}
|
||||
|
||||
if (this._isCall) {
|
||||
const x = 128 + (this._isOutgoing ? 0 : 8);
|
||||
const lengthStart = data.length;
|
||||
@ -158,7 +199,7 @@ class MTProtoState {
|
||||
data = Buffer.concat([data, Buffer.from(new Array(4 - (lengthStart % 4)).fill(0x20))]);
|
||||
}
|
||||
|
||||
const msgKeyLarge = await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||
const msgKeyLarge = await sha256(Buffer.concat([authKey
|
||||
.slice(88 + x, 88 + x + 32), Buffer.from(data)]));
|
||||
|
||||
const msgKey = msgKeyLarge.slice(8, 24);
|
||||
@ -166,19 +207,19 @@ class MTProtoState {
|
||||
const {
|
||||
iv,
|
||||
key,
|
||||
} = await this._calcKey(this.authKey.getKey(), msgKey, true);
|
||||
} = await this._calcKey(authKey, msgKey, true);
|
||||
|
||||
data = Helpers.convertToLittle(new aes.CTR(key, iv).encrypt(data));
|
||||
data = new CTR(key, iv).encrypt(data);
|
||||
// data = data.slice(0, lengthStart)
|
||||
return Buffer.concat([msgKey, data]);
|
||||
} else {
|
||||
const s = toSignedLittleBuffer(this.salt, 8);
|
||||
const i = toSignedLittleBuffer(this.id, 8);
|
||||
data = Buffer.concat([Buffer.concat([s, i]), data]);
|
||||
const padding = Helpers.generateRandomBytes(Helpers.mod(-(data.length + 12), 16) + 12);
|
||||
const padding = generateRandomBytes(mod(-(data.length + 12), 16) + 12);
|
||||
// Being substr(what, offset, length); x = 0 for client
|
||||
// "msg_key_large = SHA256(substr(auth_key, 88+x, 32) + pt + padding)"
|
||||
const msgKeyLarge = await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||
const msgKeyLarge = await sha256(Buffer.concat([authKey
|
||||
.slice(88, 88 + 32), data, padding]));
|
||||
// "msg_key = substr (msg_key_large, 8, 16)"
|
||||
const msgKey = msgKeyLarge.slice(8, 24);
|
||||
@ -186,9 +227,9 @@ class MTProtoState {
|
||||
const {
|
||||
iv,
|
||||
key,
|
||||
} = await this._calcKey(this.authKey.getKey(), msgKey, true);
|
||||
} = await this._calcKey(authKey, msgKey, true);
|
||||
|
||||
const keyId = Helpers.readBufferFromBigInt(this.authKey.keyId, 8);
|
||||
const keyId = readBufferFromBigInt(this.authKey.keyId, 8);
|
||||
return Buffer.concat([keyId, msgKey, new IGE(key, iv).encryptIge(Buffer.concat([data, padding]))]);
|
||||
}
|
||||
}
|
||||
@ -197,7 +238,11 @@ class MTProtoState {
|
||||
* Inverse of `encrypt_message_data` for incoming server messages.
|
||||
* @param body
|
||||
*/
|
||||
async decryptMessageData(body) {
|
||||
async decryptMessageData(body: Buffer) {
|
||||
if (!this.authKey) {
|
||||
throw new Error('Auth key unset');
|
||||
}
|
||||
|
||||
if (body.length < 8) {
|
||||
throw new InvalidBufferError(body);
|
||||
}
|
||||
@ -209,19 +254,23 @@ class MTProtoState {
|
||||
}
|
||||
// TODO Check salt,sessionId, and sequenceNumber
|
||||
if (!this._isCall) {
|
||||
const keyId = Helpers.readBigIntFromBuffer(body.slice(0, 8));
|
||||
const keyId = readBigIntFromBuffer(body.slice(0, 8));
|
||||
|
||||
if (keyId.neq(this.authKey.keyId)) {
|
||||
if (!this.authKey.keyId || keyId.neq(this.authKey.keyId)) {
|
||||
throw new SecurityError('Server replied with an invalid auth key');
|
||||
}
|
||||
}
|
||||
const authKey = this.authKey.getKey();
|
||||
if (!authKey) {
|
||||
throw new SecurityError('Unset AuthKey');
|
||||
}
|
||||
const msgKey = this._isCall ? body.slice(0, 16) : body.slice(8, 24);
|
||||
|
||||
const x = this._isCall ? 128 + (this.isOutgoing ? 8 : 0) : undefined;
|
||||
const x = this._isCall ? 128 + (this._isOutgoing ? 8 : 0) : 0;
|
||||
const {
|
||||
iv,
|
||||
key,
|
||||
} = await this._calcKey(this.authKey.getKey(), msgKey, false);
|
||||
} = await this._calcKey(authKey, msgKey, false);
|
||||
|
||||
if (this._isCall) {
|
||||
body = body.slice(16);
|
||||
@ -229,7 +278,7 @@ class MTProtoState {
|
||||
|
||||
body = Buffer.concat([body, Buffer.from(new Array(4 - (lengthStart % 4)).fill(0))]);
|
||||
|
||||
body = Helpers.convertToLittle(new aes.CTR(key, iv).decrypt(body));
|
||||
body = new CTR(key, iv).decrypt(body);
|
||||
|
||||
body = body.slice(0, lengthStart);
|
||||
} else {
|
||||
@ -239,9 +288,9 @@ class MTProtoState {
|
||||
// Sections "checking sha256 hash" and "message length"
|
||||
|
||||
const ourKey = this._isCall
|
||||
? await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||
? await sha256(Buffer.concat([authKey
|
||||
.slice(88 + x, 88 + x + 32), body]))
|
||||
: await Helpers.sha256(Buffer.concat([this.authKey.getKey()
|
||||
: await sha256(Buffer.concat([authKey
|
||||
.slice(96, 96 + 32), body]));
|
||||
|
||||
if (!this._isCall && !msgKey.equals(ourKey.slice(8, 24))) {
|
||||
@ -323,7 +372,7 @@ class MTProtoState {
|
||||
/**
|
||||
* Returns the understood time by the message id (server time + local offset)
|
||||
*/
|
||||
getMsgIdTimeLocal(msgId) {
|
||||
getMsgIdTimeLocal(msgId: BigInt.BigInteger) {
|
||||
if (this._lastMsgId.eq(0)) {
|
||||
// this means it's the first message sent/received so don't check yet
|
||||
return false;
|
||||
@ -336,11 +385,11 @@ class MTProtoState {
|
||||
* one given a known valid message ID.
|
||||
* @param correctMsgId {BigInteger}
|
||||
*/
|
||||
updateTimeOffset(correctMsgId) {
|
||||
updateTimeOffset(correctMsgId: BigInt.BigInteger) {
|
||||
const bad = this._getNewMsgId();
|
||||
const old = this.timeOffset;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const correct = correctMsgId.shiftRight(BigInt(32));
|
||||
const correct = correctMsgId.shiftRight(BigInt(32)).toJSNumber();
|
||||
this.timeOffset = correct - now;
|
||||
|
||||
if (this.timeOffset !== old) {
|
||||
@ -359,7 +408,7 @@ class MTProtoState {
|
||||
* @param contentRelated
|
||||
* @private
|
||||
*/
|
||||
_getSeqNo(contentRelated) {
|
||||
_getSeqNo(contentRelated: boolean) {
|
||||
if (contentRelated) {
|
||||
const result = this._sequence * 2 + 1;
|
||||
this._sequence += 1;
|
||||
@ -369,5 +418,3 @@ class MTProtoState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MTProtoState;
|
||||
@ -1,36 +0,0 @@
|
||||
const Deferred = require('../../../util/Deferred').default;
|
||||
|
||||
class RequestState {
|
||||
constructor(request, abortSignal = undefined) {
|
||||
this.containerId = undefined;
|
||||
this.msgId = undefined;
|
||||
this.request = request;
|
||||
this.data = request.getBytes();
|
||||
this.after = undefined;
|
||||
this.result = undefined;
|
||||
this.abortSignal = abortSignal;
|
||||
this.finished = new Deferred();
|
||||
|
||||
this.resetPromise();
|
||||
}
|
||||
|
||||
isReady() {
|
||||
if (!this.after) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.after.finished.promise;
|
||||
}
|
||||
|
||||
resetPromise() {
|
||||
// Prevent stuck await
|
||||
this.reject?.();
|
||||
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RequestState;
|
||||
63
src/lib/gramjs/network/RequestState.ts
Normal file
63
src/lib/gramjs/network/RequestState.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import type BigInt from 'big-integer';
|
||||
|
||||
import type { Api } from '../tl';
|
||||
|
||||
import Deferred from '../../../util/Deferred';
|
||||
|
||||
export type CallableRequest = Api.AnyRequest | Api.MsgsAck | Api.MsgsStateInfo | Api.HttpWait;
|
||||
type RequestResponse<T> = T extends { __response: infer R } ? R : void;
|
||||
|
||||
export default class RequestState<T extends CallableRequest = CallableRequest> {
|
||||
public containerId?: BigInt.BigInteger;
|
||||
|
||||
public msgId?: BigInt.BigInteger;
|
||||
|
||||
public request: any;
|
||||
|
||||
public data: Buffer;
|
||||
|
||||
public after: any;
|
||||
|
||||
public result: undefined;
|
||||
|
||||
public finished: Deferred;
|
||||
|
||||
public promise: Promise<RequestResponse<T> | undefined> | undefined;
|
||||
|
||||
public abortSignal: AbortSignal | undefined;
|
||||
|
||||
public resolve?: (value?: RequestResponse<T>) => void;
|
||||
|
||||
public reject?: (reason?: Error) => void;
|
||||
|
||||
constructor(request: T, abortSignal?: AbortSignal) {
|
||||
this.containerId = undefined;
|
||||
this.msgId = undefined;
|
||||
this.request = request;
|
||||
this.data = request.getBytes();
|
||||
this.after = undefined;
|
||||
this.result = undefined;
|
||||
this.abortSignal = abortSignal;
|
||||
this.finished = new Deferred();
|
||||
|
||||
this.resetPromise();
|
||||
}
|
||||
|
||||
isReady() {
|
||||
if (!this.after) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.after.finished.promise;
|
||||
}
|
||||
|
||||
resetPromise() {
|
||||
// Prevent stuck await
|
||||
this.reject?.();
|
||||
|
||||
this.promise = new Promise<RequestResponse<T> | undefined>((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,18 @@
|
||||
const PromisedWebSockets = require('../../extensions/PromisedWebSockets');
|
||||
const HttpStream = require('../../extensions/HttpStream').default;
|
||||
const AsyncQueue = require('../../extensions/AsyncQueue');
|
||||
import type { Logger } from '../../extensions';
|
||||
import type { AbridgedPacketCodec } from './TCPAbridged';
|
||||
|
||||
import { AsyncQueue, PromisedWebSockets } from '../../extensions';
|
||||
|
||||
import HttpStream from '../../extensions/HttpStream';
|
||||
|
||||
interface ConnectionInterfaceParams {
|
||||
ip: string;
|
||||
port: number;
|
||||
dcId: number;
|
||||
loggers: Logger;
|
||||
isPremium?: boolean;
|
||||
isTestServer?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Connection` class is a wrapper around ``asyncio.open_connection``.
|
||||
@ -13,15 +25,47 @@ const AsyncQueue = require('../../extensions/AsyncQueue');
|
||||
* ``ConnectionError``, which will raise when attempting to send if
|
||||
* the client is disconnected (includes remote disconnections).
|
||||
*/
|
||||
class Connection {
|
||||
PacketCodecClass = undefined;
|
||||
export class Connection {
|
||||
PacketCodecClass?: typeof AbridgedPacketCodec;
|
||||
|
||||
constructor(ip, port, dcId, loggers, testServers, isPremium) {
|
||||
readonly _ip: string;
|
||||
|
||||
readonly _port: number;
|
||||
|
||||
_dcId: number;
|
||||
|
||||
_log: Logger;
|
||||
|
||||
_connected: boolean;
|
||||
|
||||
_isPremium?: boolean;
|
||||
|
||||
shouldLongPoll: boolean;
|
||||
|
||||
private _sendTask?: Promise<void>;
|
||||
|
||||
private _recvTask?: Promise<void>;
|
||||
|
||||
protected _codec: any;
|
||||
|
||||
protected _obfuscation: any;
|
||||
|
||||
_sendArray: AsyncQueue<Buffer>;
|
||||
|
||||
_recvArray: AsyncQueue<Buffer | undefined>;
|
||||
|
||||
socket: PromisedWebSockets | HttpStream;
|
||||
|
||||
public _isTestServer?: boolean;
|
||||
|
||||
constructor({
|
||||
ip, port, dcId, loggers, isPremium, isTestServer,
|
||||
}: ConnectionInterfaceParams) {
|
||||
this._ip = ip;
|
||||
this._port = port;
|
||||
this._dcId = dcId;
|
||||
this._log = loggers;
|
||||
this._testServers = testServers;
|
||||
this._isTestServer = isTestServer;
|
||||
|
||||
this._isPremium = isPremium;
|
||||
this._connected = false;
|
||||
@ -29,8 +73,8 @@ class Connection {
|
||||
this._recvTask = undefined;
|
||||
this._codec = undefined;
|
||||
this._obfuscation = undefined; // TcpObfuscated and MTProxy
|
||||
this._sendArray = new AsyncQueue();
|
||||
this._recvArray = new AsyncQueue();
|
||||
this._sendArray = new AsyncQueue<Buffer>();
|
||||
this._recvArray = new AsyncQueue<Buffer>();
|
||||
// this.socket = new PromiseSocket(new Socket())
|
||||
|
||||
this.shouldLongPoll = false;
|
||||
@ -47,10 +91,10 @@ class Connection {
|
||||
|
||||
async _connect() {
|
||||
this._log.debug('Connecting');
|
||||
this._codec = new this.PacketCodecClass(this);
|
||||
await this.socket.connect(this._port, this._ip, this._testServers, this._isPremium);
|
||||
this._codec = new this.PacketCodecClass!(this);
|
||||
await this.socket.connect(this._port, this._ip, this._isTestServer, this._isPremium);
|
||||
this._log.debug('Finished connecting');
|
||||
// await this.socket.connect({host: this._ip, port: this._port});
|
||||
|
||||
await this._initConn();
|
||||
}
|
||||
|
||||
@ -76,7 +120,7 @@ class Connection {
|
||||
}
|
||||
}
|
||||
|
||||
async send(data) {
|
||||
async send(data: Buffer) {
|
||||
if (!this._connected) {
|
||||
throw new Error('Not connected');
|
||||
}
|
||||
@ -135,7 +179,7 @@ class Connection {
|
||||
}
|
||||
}
|
||||
|
||||
_send(data) {
|
||||
_send(data: Buffer) {
|
||||
const encodedPacket = this._codec.encodePacket(data);
|
||||
this.socket.write(encodedPacket);
|
||||
}
|
||||
@ -149,15 +193,15 @@ class Connection {
|
||||
}
|
||||
}
|
||||
|
||||
class ObfuscatedConnection extends Connection {
|
||||
ObfuscatedIO = undefined;
|
||||
export class ObfuscatedConnection extends Connection {
|
||||
ObfuscatedIO: any = undefined;
|
||||
|
||||
_initConn() {
|
||||
async _initConn() {
|
||||
this._obfuscation = new this.ObfuscatedIO(this);
|
||||
this.socket.write(this._obfuscation.header);
|
||||
}
|
||||
|
||||
_send(data) {
|
||||
_send(data: Buffer) {
|
||||
this._obfuscation.write(this._codec.encodePacket(data));
|
||||
}
|
||||
|
||||
@ -166,34 +210,40 @@ class ObfuscatedConnection extends Connection {
|
||||
}
|
||||
}
|
||||
|
||||
class PacketCodec {
|
||||
constructor(connection) {
|
||||
export class PacketCodec {
|
||||
private _conn: Buffer;
|
||||
|
||||
constructor(connection: Buffer) {
|
||||
this._conn = connection;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
encodePacket(data) {
|
||||
encodePacket(data: Buffer) {
|
||||
throw new Error('Not Implemented');
|
||||
|
||||
// Override
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
readPacket(reader) {
|
||||
readPacket(reader: PromisedWebSockets) {
|
||||
// override
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
}
|
||||
|
||||
class HttpConnection extends Connection {
|
||||
constructor(ip, port, dcId, loggers, testServers, isPremium) {
|
||||
super(ip, port, dcId, loggers, testServers, isPremium);
|
||||
export class HttpConnection extends Connection {
|
||||
socket: HttpStream;
|
||||
|
||||
href: string;
|
||||
|
||||
constructor(params: ConnectionInterfaceParams) {
|
||||
super(params);
|
||||
this.shouldLongPoll = true;
|
||||
this.socket = new HttpStream(this.disconnectCallback.bind(this));
|
||||
this.href = HttpStream.getURL(this._ip, this._port, this._testServers, this._isPremium);
|
||||
this.href = HttpStream.getURL(this._ip, this._port, this._isTestServer, this._isPremium);
|
||||
}
|
||||
|
||||
send(data) {
|
||||
send(data: Buffer) {
|
||||
return this.socket.write(data);
|
||||
}
|
||||
|
||||
@ -203,7 +253,7 @@ class HttpConnection extends Connection {
|
||||
|
||||
async _connect() {
|
||||
this._log.debug('Connecting');
|
||||
await this.socket.connect(this._port, this._ip, this._testServers, this._isPremium);
|
||||
await this.socket.connect(this._port, this._ip, this._isTestServer, this._isPremium);
|
||||
this._log.debug('Finished connecting');
|
||||
}
|
||||
|
||||
@ -212,10 +262,3 @@ class HttpConnection extends Connection {
|
||||
this._connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Connection,
|
||||
PacketCodec,
|
||||
ObfuscatedConnection,
|
||||
HttpConnection,
|
||||
};
|
||||
@ -1,34 +1,39 @@
|
||||
const BigInt = require('big-integer');
|
||||
const { readBufferFromBigInt } = require('../../Helpers');
|
||||
const {
|
||||
Connection,
|
||||
PacketCodec,
|
||||
} = require('./Connection');
|
||||
import BigInt from 'big-integer';
|
||||
|
||||
class AbridgedPacketCodec extends PacketCodec {
|
||||
import type { PromisedWebSockets } from '../../extensions';
|
||||
|
||||
import { readBufferFromBigInt } from '../../Helpers';
|
||||
import { Connection, PacketCodec } from './Connection';
|
||||
|
||||
export class AbridgedPacketCodec extends PacketCodec {
|
||||
static tag = Buffer.from('ef', 'hex');
|
||||
|
||||
static obfuscateTag = Buffer.from('efefefef', 'hex');
|
||||
|
||||
constructor(props) {
|
||||
private tag: Buffer;
|
||||
|
||||
obfuscateTag: Buffer;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.tag = AbridgedPacketCodec.tag;
|
||||
this.obfuscateTag = AbridgedPacketCodec.obfuscateTag;
|
||||
}
|
||||
|
||||
encodePacket(data) {
|
||||
let length = data.length >> 2;
|
||||
encodePacket(data: Buffer) {
|
||||
const length = data.length >> 2;
|
||||
let temp;
|
||||
if (length < 127) {
|
||||
const b = Buffer.alloc(1);
|
||||
b.writeUInt8(length, 0);
|
||||
length = b;
|
||||
temp = b;
|
||||
} else {
|
||||
length = Buffer.concat([Buffer.from('7f', 'hex'), readBufferFromBigInt(BigInt(length), 3)]);
|
||||
temp = Buffer.concat([Buffer.from('7f', 'hex'), readBufferFromBigInt(BigInt(length), 3)]);
|
||||
}
|
||||
return Buffer.concat([length, data]);
|
||||
return Buffer.concat([temp, data]);
|
||||
}
|
||||
|
||||
async readPacket(reader) {
|
||||
async readPacket(reader: PromisedWebSockets) {
|
||||
const readData = await reader.read(1);
|
||||
let length = readData[0];
|
||||
if (length >= 127) {
|
||||
@ -45,11 +50,6 @@ class AbridgedPacketCodec extends PacketCodec {
|
||||
* only require 1 byte if the packet length is less than
|
||||
* 508 bytes (127 << 2, which is very common).
|
||||
*/
|
||||
class ConnectionTCPAbridged extends Connection {
|
||||
export class ConnectionTCPAbridged extends Connection {
|
||||
PacketCodecClass = AbridgedPacketCodec;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ConnectionTCPAbridged,
|
||||
AbridgedPacketCodec,
|
||||
};
|
||||
@ -1,57 +0,0 @@
|
||||
// CONTEST
|
||||
// const { Connection, PacketCodec } = require('./Connection')
|
||||
// const { crc32 } = require('../../Helpers')
|
||||
// const { InvalidChecksumError } = require('../../errors/Common')
|
||||
//
|
||||
// class FullPacketCodec extends PacketCodec {
|
||||
// constructor(connection) {
|
||||
// super(connection)
|
||||
// this._sendCounter = 0 // Telegram will ignore us otherwise
|
||||
// }
|
||||
//
|
||||
// encodePacket(data) {
|
||||
// // https://core.telegram.org/mtproto#tcp-transport
|
||||
// // total length, sequence number, packet and checksum (CRC32)
|
||||
// const length = data.length + 12
|
||||
// const e = Buffer.alloc(8)
|
||||
// e.writeInt32LE(length,0)
|
||||
// e.writeInt32LE(this._sendCounter,4)
|
||||
// data = Buffer.concat([e, data])
|
||||
// const crc = Buffer.alloc(4)
|
||||
// crc.writeUInt32LE(crc32(data),0)
|
||||
// this._sendCounter += 1
|
||||
// return Buffer.concat([data, crc])
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// *
|
||||
// * @param reader {PromisedWebSockets}
|
||||
// * @returns {Promise<*>}
|
||||
// */
|
||||
// async readPacket(reader) {
|
||||
// const packetLenSeq = await reader.read(8) // 4 and 4
|
||||
// // process.exit(0);
|
||||
// if (packetLenSeq === undefined) {
|
||||
// return false
|
||||
// }
|
||||
// const packetLen = packetLenSeq.readInt32LE(0)
|
||||
// let body = await reader.read(packetLen - 8)
|
||||
// const [checksum] = body.slice(-4).readUInt32LE(0)
|
||||
// body = body.slice(0, -4)
|
||||
//
|
||||
// const validChecksum = crc32(Buffer.concat([packetLenSeq, body]))
|
||||
// if (!(validChecksum === checksum)) {
|
||||
// throw new InvalidChecksumError(checksum, validChecksum)
|
||||
// }
|
||||
// return body
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// class ConnectionTCPFull extends Connection {
|
||||
// PacketCodecClass = FullPacketCodec;
|
||||
// }
|
||||
//
|
||||
// module.exports = {
|
||||
// FullPacketCodec,
|
||||
// ConnectionTCPFull,
|
||||
// }
|
||||
@ -1,12 +1,21 @@
|
||||
const { generateRandomBytes } = require('../../Helpers');
|
||||
const { ObfuscatedConnection } = require('./Connection');
|
||||
const { AbridgedPacketCodec } = require('./TCPAbridged');
|
||||
const CTR = require('../../crypto/CTR');
|
||||
import type { HttpStream, PromisedWebSockets } from '../../extensions';
|
||||
|
||||
import { CTR } from '../../crypto/CTR';
|
||||
|
||||
import { generateRandomBytes } from '../../Helpers';
|
||||
import { ObfuscatedConnection } from './Connection';
|
||||
import { AbridgedPacketCodec } from './TCPAbridged';
|
||||
|
||||
class ObfuscatedIO {
|
||||
header = undefined;
|
||||
header?: Buffer = undefined;
|
||||
|
||||
constructor(connection) {
|
||||
private connection: PromisedWebSockets | HttpStream;
|
||||
|
||||
private _encrypt: CTR;
|
||||
|
||||
private _decrypt: CTR;
|
||||
|
||||
constructor(connection: ConnectionTCPObfuscated) {
|
||||
this.connection = connection.socket;
|
||||
const res = this.initHeader(connection.PacketCodecClass);
|
||||
this.header = res.random;
|
||||
@ -15,10 +24,14 @@ class ObfuscatedIO {
|
||||
this._decrypt = res.decryptor;
|
||||
}
|
||||
|
||||
initHeader(packetCodec) {
|
||||
initHeader(packetCodec: typeof AbridgedPacketCodec) {
|
||||
// Obfuscated messages secrets cannot start with any of these
|
||||
const keywords = [Buffer.from('50567247', 'hex'), Buffer.from('474554', 'hex'),
|
||||
Buffer.from('504f5354', 'hex'), Buffer.from('eeeeeeee', 'hex')];
|
||||
const keywords = [
|
||||
Buffer.from('50567247', 'hex'),
|
||||
Buffer.from('474554', 'hex'),
|
||||
Buffer.from('504f5354', 'hex'),
|
||||
Buffer.from('eeeeeeee', 'hex'),
|
||||
];
|
||||
let random;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
@ -64,22 +77,18 @@ class ObfuscatedIO {
|
||||
};
|
||||
}
|
||||
|
||||
async read(n) {
|
||||
async read(n: number) {
|
||||
const data = await this.connection.readExactly(n);
|
||||
return this._decrypt.encrypt(data);
|
||||
}
|
||||
|
||||
write(data) {
|
||||
write(data: Buffer) {
|
||||
this.connection.write(this._encrypt.encrypt(data));
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionTCPObfuscated extends ObfuscatedConnection {
|
||||
export class ConnectionTCPObfuscated extends ObfuscatedConnection {
|
||||
ObfuscatedIO = ObfuscatedIO;
|
||||
|
||||
PacketCodecClass = AbridgedPacketCodec;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ConnectionTCPObfuscated,
|
||||
};
|
||||
@ -1,12 +0,0 @@
|
||||
const { Connection, HttpConnection } = require('./Connection');
|
||||
const { ConnectionTCPFull } = require('./TCPFull');
|
||||
const { ConnectionTCPAbridged } = require('./TCPAbridged');
|
||||
const { ConnectionTCPObfuscated } = require('./TCPObfuscated');
|
||||
|
||||
module.exports = {
|
||||
Connection,
|
||||
HttpConnection,
|
||||
ConnectionTCPFull,
|
||||
ConnectionTCPAbridged,
|
||||
ConnectionTCPObfuscated,
|
||||
};
|
||||
3
src/lib/gramjs/network/connection/index.ts
Normal file
3
src/lib/gramjs/network/connection/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { Connection, HttpConnection } from './Connection';
|
||||
export { ConnectionTCPAbridged } from './TCPAbridged';
|
||||
export { ConnectionTCPObfuscated } from './TCPObfuscated';
|
||||
@ -1,27 +0,0 @@
|
||||
const MTProtoPlainSender = require('./MTProtoPlainSender');
|
||||
const MTProtoSender = require('./MTProtoSender');
|
||||
|
||||
const {
|
||||
Connection,
|
||||
ConnectionTCPFull,
|
||||
ConnectionTCPAbridged,
|
||||
ConnectionTCPObfuscated,
|
||||
HttpConnection,
|
||||
} = require('./connection');
|
||||
|
||||
const {
|
||||
UpdateConnectionState,
|
||||
UpdateServerTimeOffset,
|
||||
} = require('./updates');
|
||||
|
||||
module.exports = {
|
||||
Connection,
|
||||
HttpConnection,
|
||||
ConnectionTCPFull,
|
||||
ConnectionTCPAbridged,
|
||||
ConnectionTCPObfuscated,
|
||||
MTProtoPlainSender,
|
||||
MTProtoSender,
|
||||
UpdateConnectionState,
|
||||
UpdateServerTimeOffset,
|
||||
};
|
||||
18
src/lib/gramjs/network/index.ts
Normal file
18
src/lib/gramjs/network/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {
|
||||
Connection, ConnectionTCPAbridged, ConnectionTCPObfuscated, HttpConnection,
|
||||
} from './connection';
|
||||
import { UpdateConnectionState, UpdateServerTimeOffset } from './updates';
|
||||
|
||||
import MTProtoPlainSender from './MTProtoPlainSender';
|
||||
import MTProtoSender from './MTProtoSender';
|
||||
|
||||
export {
|
||||
Connection,
|
||||
HttpConnection,
|
||||
ConnectionTCPAbridged,
|
||||
ConnectionTCPObfuscated,
|
||||
MTProtoPlainSender,
|
||||
MTProtoSender,
|
||||
UpdateConnectionState,
|
||||
UpdateServerTimeOffset,
|
||||
};
|
||||
@ -1,23 +0,0 @@
|
||||
class UpdateConnectionState {
|
||||
static disconnected = -1;
|
||||
|
||||
static connected = 1;
|
||||
|
||||
static broken = 0;
|
||||
|
||||
constructor(state, origin) {
|
||||
this.state = state;
|
||||
this.origin = origin;
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateServerTimeOffset {
|
||||
constructor(timeOffset) {
|
||||
this.timeOffset = timeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UpdateConnectionState,
|
||||
UpdateServerTimeOffset,
|
||||
};
|
||||
21
src/lib/gramjs/network/updates.ts
Normal file
21
src/lib/gramjs/network/updates.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export class UpdateConnectionState {
|
||||
static disconnected = -1;
|
||||
|
||||
static connected = 1;
|
||||
|
||||
static broken = 0;
|
||||
|
||||
state: number;
|
||||
|
||||
constructor(state: number) {
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateServerTimeOffset {
|
||||
timeOffset: number;
|
||||
|
||||
constructor(timeOffset: number) {
|
||||
this.timeOffset = timeOffset;
|
||||
}
|
||||
}
|
||||
@ -1,174 +0,0 @@
|
||||
class Session {
|
||||
/**
|
||||
* Creates a clone of this session file
|
||||
* @param toInstance {Session|null}
|
||||
* @returns {Session}
|
||||
*/
|
||||
|
||||
/* CONTEST
|
||||
clone(toInstance = null) {
|
||||
return toInstance || new this.constructor()
|
||||
} */
|
||||
|
||||
/**
|
||||
* Returns the currently-used data center ID.
|
||||
*/
|
||||
get dcId() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the server address where the library should connect to.
|
||||
*/
|
||||
get serverAddress() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the port to which the library should connect to.
|
||||
*/
|
||||
get port() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ``AuthKey`` instance associated with the saved
|
||||
* data center, or `None` if a new one should be generated.
|
||||
*/
|
||||
get authKey() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ``AuthKey`` to be used for the saved data center.
|
||||
* @param value
|
||||
*/
|
||||
set authKey(value) {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the information of the data center address and port that
|
||||
* the library should connect to, as well as the data center ID,
|
||||
* which is currently unused.
|
||||
* @param dcId {number}
|
||||
* @param serverAddress {string}
|
||||
* @param port {number}
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
setDC(dcId, serverAddress, port) {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ID of the takeout process initialized for this session,
|
||||
* or `None` if there's no were any unfinished takeout requests.
|
||||
*/
|
||||
/* CONTEST
|
||||
get takeoutId() {
|
||||
throw new Error('Not Implemented')
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Sets the ID of the unfinished takeout process for this session.
|
||||
* @param value
|
||||
*/
|
||||
/* CONTEST
|
||||
set takeoutId(value) {
|
||||
throw new Error('Not Implemented')
|
||||
}
|
||||
*/
|
||||
/**
|
||||
* Returns the ``UpdateState`` associated with the given `entity_id`.
|
||||
* If the `entity_id` is 0, it should return the ``UpdateState`` for
|
||||
* no specific channel (the "general" state). If no state is known
|
||||
* it should ``return None``.
|
||||
* @param entityId
|
||||
*/
|
||||
/* CONTEST
|
||||
getUpdateState(entityId) {
|
||||
throw new Error('Not Implemented')
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the given ``UpdateState`` for the specified `entity_id`, which
|
||||
* should be 0 if the ``UpdateState`` is the "general" state (and not
|
||||
* for any specific channel).
|
||||
* @param entityId
|
||||
* @param state
|
||||
*/
|
||||
/* CONTEST
|
||||
setUpdateState(entityId, state) {
|
||||
throw new Error('Not Implemented')
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called on client disconnection. Should be used to
|
||||
* free any used resources. Can be left empty if none.
|
||||
*/
|
||||
|
||||
/* CONTEST
|
||||
close() {
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* called whenever important properties change. It should
|
||||
* make persist the relevant session information to disk.
|
||||
*/
|
||||
save() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called upon client.log_out(). Should delete the stored
|
||||
* information from disk since it's not valid anymore.
|
||||
*/
|
||||
|
||||
delete() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available sessions. Not used by the library itself.
|
||||
*/
|
||||
/* CONTEST
|
||||
listSessions() {
|
||||
throw new Error('Not Implemented')
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processes the input ``TLObject`` or ``list`` and saves
|
||||
* whatever information is relevant (e.g., ID or access hash).
|
||||
* @param tlo
|
||||
*/
|
||||
/* CONTEST
|
||||
processEntities(tlo) {
|
||||
throw new Error('Not Implemented')
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* Turns the given key into an ``InputPeer`` (e.g. ``InputPeerUser``).
|
||||
* The library uses this method whenever an ``InputPeer`` is needed
|
||||
* to suit several purposes (e.g. user only provided its ID or wishes
|
||||
* to use a cached username to avoid extra RPC).
|
||||
*/
|
||||
/* CONTEST
|
||||
getInputEntity(key) {
|
||||
throw new Error('Not Implemented')
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports = Session;
|
||||
23
src/lib/gramjs/sessions/Abstract.ts
Normal file
23
src/lib/gramjs/sessions/Abstract.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import type { AuthKey } from '../crypto/AuthKey';
|
||||
|
||||
export default abstract class Session {
|
||||
abstract setDC(dcId: number, serverAddress: string, port: number, isTestServer?: boolean): void;
|
||||
|
||||
abstract get dcId(): number;
|
||||
|
||||
abstract get serverAddress(): string;
|
||||
|
||||
abstract get port(): number;
|
||||
|
||||
abstract get isTestServer(): boolean | undefined;
|
||||
|
||||
abstract getAuthKey(dcId?: number): AuthKey;
|
||||
|
||||
abstract setAuthKey(authKey: AuthKey | undefined, dcId?: number): void;
|
||||
|
||||
abstract save(): void;
|
||||
|
||||
abstract load(): Promise<void>;
|
||||
|
||||
abstract delete(): void;
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
/* eslint-disable no-restricted-globals */
|
||||
const StorageSession = require('./StorageSession');
|
||||
|
||||
const CACHE_NAME = 'GramJs';
|
||||
|
||||
class CacheApiSession extends StorageSession {
|
||||
async _delete() {
|
||||
const request = new Request(this._storageKey);
|
||||
const cache = await self.caches.open(CACHE_NAME);
|
||||
return cache.delete(request);
|
||||
}
|
||||
|
||||
async _fetchFromCache() {
|
||||
const request = new Request(this._storageKey);
|
||||
const cache = await self.caches.open(CACHE_NAME);
|
||||
const cached = await cache.match(request);
|
||||
return cached ? cached.text() : undefined;
|
||||
}
|
||||
|
||||
async _saveToCache(data) {
|
||||
const request = new Request(this._storageKey);
|
||||
const response = new Response(data);
|
||||
const cache = await self.caches.open(CACHE_NAME);
|
||||
return cache.put(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CacheApiSession;
|
||||
@ -1,9 +1,17 @@
|
||||
const MemorySession = require('./Memory');
|
||||
const AuthKey = require('../crypto/AuthKey');
|
||||
const utils = require('../Utils');
|
||||
import type { SessionData } from '../types';
|
||||
|
||||
class CallbackSession extends MemorySession {
|
||||
constructor(sessionData, callback) {
|
||||
import { AuthKey } from '../crypto/AuthKey';
|
||||
import { getDC } from '../Utils';
|
||||
import MemorySession from './Memory';
|
||||
|
||||
export default class CallbackSession extends MemorySession {
|
||||
private _sessionData?: SessionData;
|
||||
|
||||
private _callback: (session?: SessionData) => void;
|
||||
|
||||
private _authKeys: Record<number, AuthKey>;
|
||||
|
||||
constructor(sessionData: SessionData | undefined, callback: (session?: SessionData) => void) {
|
||||
super();
|
||||
|
||||
this._sessionData = sessionData;
|
||||
@ -12,14 +20,6 @@ class CallbackSession extends MemorySession {
|
||||
this._authKeys = {};
|
||||
}
|
||||
|
||||
get authKey() {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
set authKey(value) {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (!this._sessionData) {
|
||||
return;
|
||||
@ -34,30 +34,31 @@ class CallbackSession extends MemorySession {
|
||||
const {
|
||||
ipAddress,
|
||||
port,
|
||||
} = utils.getDC(mainDcId);
|
||||
} = getDC(mainDcId);
|
||||
|
||||
this.setDC(mainDcId, ipAddress, port, isTest, true);
|
||||
|
||||
await Promise.all(Object.keys(keys)
|
||||
.map(async (dcId) => {
|
||||
.map(async (dcIdStr) => {
|
||||
const dcId = Number(dcIdStr);
|
||||
const key = typeof keys[dcId] === 'string'
|
||||
? Buffer.from(keys[dcId], 'hex')
|
||||
? Buffer.from(keys[dcId] as string, 'hex')
|
||||
: Buffer.from(keys[dcId]);
|
||||
|
||||
if (hashes[dcId]) {
|
||||
const hash = typeof hashes[dcId] === 'string'
|
||||
? Buffer.from(hashes[dcId], 'hex')
|
||||
? Buffer.from(hashes[dcId] as string, 'hex')
|
||||
: Buffer.from(hashes[dcId]);
|
||||
|
||||
this._authKeys[dcId] = new AuthKey(key, hash);
|
||||
} else {
|
||||
this._authKeys[dcId] = new AuthKey();
|
||||
await this._authKeys[dcId].setKey(key, true);
|
||||
await this._authKeys[dcId].setKey(key);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
setDC(dcId, serverAddress, port, isTestServer, skipOnUpdate = false) {
|
||||
setDC(dcId: number, serverAddress: string, port: number, isTestServer?: boolean, skipOnUpdate = false) {
|
||||
this._dcId = dcId;
|
||||
this._serverAddress = serverAddress;
|
||||
this._port = port;
|
||||
@ -74,14 +75,14 @@ class CallbackSession extends MemorySession {
|
||||
return this._authKeys[dcId];
|
||||
}
|
||||
|
||||
setAuthKey(authKey, dcId = this._dcId) {
|
||||
setAuthKey(authKey: AuthKey, dcId = this._dcId) {
|
||||
this._authKeys[dcId] = authKey;
|
||||
|
||||
void this._onUpdate();
|
||||
}
|
||||
|
||||
getSessionData() {
|
||||
const sessionData = {
|
||||
const sessionData: SessionData = {
|
||||
mainDcId: this._dcId,
|
||||
keys: {},
|
||||
hashes: {},
|
||||
@ -90,12 +91,13 @@ class CallbackSession extends MemorySession {
|
||||
|
||||
Object
|
||||
.keys(this._authKeys)
|
||||
.forEach((dcId) => {
|
||||
.forEach((dcIdStr) => {
|
||||
const dcId = Number(dcIdStr);
|
||||
const authKey = this._authKeys[dcId];
|
||||
if (!authKey || !authKey._key) return;
|
||||
if (!authKey?._key) return;
|
||||
|
||||
sessionData.keys[dcId] = authKey._key.toString('hex');
|
||||
sessionData.hashes[dcId] = authKey._hash.toString('hex');
|
||||
sessionData.hashes[dcId] = authKey._hash!.toString('hex');
|
||||
});
|
||||
|
||||
return sessionData;
|
||||
@ -109,5 +111,3 @@ class CallbackSession extends MemorySession {
|
||||
this._callback(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CallbackSession;
|
||||
@ -1,20 +0,0 @@
|
||||
const idb = require('idb-keyval');
|
||||
const StorageSession = require('./StorageSession');
|
||||
|
||||
const CACHE_NAME = 'GramJs';
|
||||
|
||||
class IdbSession extends StorageSession {
|
||||
_delete() {
|
||||
return idb.del(`${CACHE_NAME}:${this._storageKey}`);
|
||||
}
|
||||
|
||||
_fetchFromCache() {
|
||||
return idb.get(`${CACHE_NAME}:${this._storageKey}`);
|
||||
}
|
||||
|
||||
_saveToCache(data) {
|
||||
return idb.set(`${CACHE_NAME}:${this._storageKey}`, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = IdbSession;
|
||||
@ -1,17 +0,0 @@
|
||||
const StorageSession = require('./StorageSession');
|
||||
|
||||
class LocalStorageSession extends StorageSession {
|
||||
_delete() {
|
||||
return localStorage.removeItem(this._storageKey);
|
||||
}
|
||||
|
||||
_fetchFromCache() {
|
||||
return localStorage.getItem(this._storageKey);
|
||||
}
|
||||
|
||||
_saveToCache(data) {
|
||||
return localStorage.setItem(this._storageKey, data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalStorageSession;
|
||||
@ -1,49 +0,0 @@
|
||||
const Session = require('./Abstract');
|
||||
|
||||
class MemorySession extends Session {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._serverAddress = undefined;
|
||||
this._dcId = 0;
|
||||
this._port = undefined;
|
||||
this._takeoutId = undefined;
|
||||
this._isTestServer = false;
|
||||
|
||||
this._entities = new Set();
|
||||
this._updateStates = {};
|
||||
}
|
||||
|
||||
get dcId() {
|
||||
return this._dcId;
|
||||
}
|
||||
|
||||
get serverAddress() {
|
||||
return this._serverAddress;
|
||||
}
|
||||
|
||||
get port() {
|
||||
return this._port;
|
||||
}
|
||||
|
||||
get authKey() {
|
||||
return this._authKey;
|
||||
}
|
||||
|
||||
set authKey(value) {
|
||||
this._authKey = value;
|
||||
}
|
||||
|
||||
get isTestServer() {
|
||||
return this._isTestServer;
|
||||
}
|
||||
|
||||
setDC(dcId, serverAddress, port, isTestServer) {
|
||||
this._dcId = dcId | 0;
|
||||
this._serverAddress = serverAddress;
|
||||
this._port = port;
|
||||
this._isTestServer = isTestServer;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MemorySession;
|
||||
67
src/lib/gramjs/sessions/Memory.ts
Normal file
67
src/lib/gramjs/sessions/Memory.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { AuthKey } from '../crypto/AuthKey';
|
||||
import Session from './Abstract';
|
||||
|
||||
// Dummy implementation
|
||||
export default class MemorySession extends Session {
|
||||
protected _serverAddress?: string;
|
||||
|
||||
protected _dcId: number;
|
||||
|
||||
protected _port?: number;
|
||||
|
||||
protected _takeoutId: undefined;
|
||||
|
||||
protected _entities: Set<any>;
|
||||
|
||||
protected _updateStates: {};
|
||||
|
||||
protected _isTestServer?: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._serverAddress = undefined;
|
||||
this._dcId = 0;
|
||||
this._port = undefined;
|
||||
this._takeoutId = undefined;
|
||||
this._isTestServer = false;
|
||||
|
||||
this._entities = new Set();
|
||||
this._updateStates = {};
|
||||
}
|
||||
|
||||
get dcId() {
|
||||
return this._dcId;
|
||||
}
|
||||
|
||||
get serverAddress() {
|
||||
return this._serverAddress!;
|
||||
}
|
||||
|
||||
get port() {
|
||||
return this._port!;
|
||||
}
|
||||
|
||||
get isTestServer() {
|
||||
return this._isTestServer;
|
||||
}
|
||||
|
||||
setDC(dcId: number, serverAddress: string, port: number, isTestServer?: boolean) {
|
||||
this._dcId = dcId | 0;
|
||||
this._serverAddress = serverAddress;
|
||||
this._port = port;
|
||||
this._isTestServer = isTestServer;
|
||||
}
|
||||
|
||||
getAuthKey(dcId?: number | undefined): AuthKey {
|
||||
return new AuthKey();
|
||||
}
|
||||
|
||||
setAuthKey(authKey: AuthKey, dcId?: number) {}
|
||||
|
||||
async load(): Promise<void> { }
|
||||
|
||||
save() {}
|
||||
|
||||
delete() {}
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
const MemorySession = require('./Memory');
|
||||
const AuthKey = require('../crypto/AuthKey');
|
||||
const utils = require('../Utils');
|
||||
|
||||
const STORAGE_KEY_BASE = 'GramJs-session-';
|
||||
const SESSION_DATA_PREFIX = 'session:';
|
||||
|
||||
class StorageSession extends MemorySession {
|
||||
constructor(sessionInfo) {
|
||||
super();
|
||||
|
||||
this._authKeys = {};
|
||||
|
||||
if (sessionInfo && sessionInfo.startsWith(SESSION_DATA_PREFIX)) {
|
||||
this._sessionString = sessionInfo;
|
||||
} else if (sessionInfo) {
|
||||
this._storageKey = sessionInfo;
|
||||
}
|
||||
}
|
||||
|
||||
get authKey() {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
set authKey(value) {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this._sessionString) {
|
||||
await this._loadFromSessionString();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._storageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const json = await this._fetchFromCache();
|
||||
const {
|
||||
mainDcId,
|
||||
keys,
|
||||
hashes,
|
||||
} = JSON.parse(json);
|
||||
const {
|
||||
ipAddress,
|
||||
port,
|
||||
} = utils.getDC(mainDcId);
|
||||
|
||||
this.setDC(mainDcId, ipAddress, port, true);
|
||||
|
||||
Object.keys(keys)
|
||||
.forEach((dcId) => {
|
||||
if (keys[dcId] && hashes[dcId]) {
|
||||
this._authKeys[dcId] = new AuthKey(
|
||||
Buffer.from(keys[dcId].data),
|
||||
Buffer.from(hashes[dcId].data),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Failed to retrieve or parse session from storage');
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
setDC(dcId, serverAddress, port, skipUpdateStorage = false) {
|
||||
this._dcId = dcId;
|
||||
this._serverAddress = serverAddress;
|
||||
this._port = port;
|
||||
|
||||
delete this._authKeys[dcId];
|
||||
|
||||
if (!skipUpdateStorage) {
|
||||
void this._updateStorage();
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
if (!this._storageKey) {
|
||||
this._storageKey = generateStorageKey();
|
||||
}
|
||||
|
||||
await this._updateStorage();
|
||||
|
||||
return this._storageKey;
|
||||
}
|
||||
|
||||
getAuthKey(dcId = this._dcId) {
|
||||
return this._authKeys[dcId];
|
||||
}
|
||||
|
||||
setAuthKey(authKey, dcId = this._dcId) {
|
||||
this._authKeys[dcId] = authKey;
|
||||
|
||||
void this._updateStorage();
|
||||
}
|
||||
|
||||
getSessionData(asHex) {
|
||||
const sessionData = {
|
||||
mainDcId: this._dcId,
|
||||
keys: {},
|
||||
hashes: {},
|
||||
};
|
||||
|
||||
Object
|
||||
.keys(this._authKeys)
|
||||
.forEach((dcId) => {
|
||||
const authKey = this._authKeys[dcId];
|
||||
if (!authKey._key) return;
|
||||
|
||||
sessionData.keys[dcId] = asHex ? authKey._key.toString('hex') : authKey._key;
|
||||
sessionData.hashes[dcId] = asHex ? authKey._hash.toString('hex') : authKey._hash;
|
||||
});
|
||||
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
async _loadFromSessionString() {
|
||||
const [, mainDcIdStr, mainDcKey] = this._sessionString.split(':');
|
||||
const mainDcId = Number(mainDcIdStr);
|
||||
const {
|
||||
ipAddress,
|
||||
port,
|
||||
} = utils.getDC(mainDcId);
|
||||
this.setDC(mainDcId, ipAddress, port);
|
||||
const authKey = new AuthKey();
|
||||
await authKey.setKey(Buffer.from(mainDcKey, 'hex'), true);
|
||||
this.setAuthKey(authKey, mainDcId);
|
||||
}
|
||||
|
||||
async _updateStorage() {
|
||||
if (!this._storageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._saveToCache(JSON.stringify(this.getSessionData()));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Failed to update session in storage');
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
async delete() {
|
||||
try {
|
||||
const deleted = await this._delete();
|
||||
return deleted;
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Failed to delete session from storage');
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(err);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// @abstract
|
||||
_delete() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
// @abstract
|
||||
_fetchFromCache() {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
|
||||
// @abstract
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_saveToCache(data) {
|
||||
throw new Error('Not Implemented');
|
||||
}
|
||||
}
|
||||
|
||||
function generateStorageKey() {
|
||||
// Creating two sessions at the same moment is not expected nor supported.
|
||||
return `${STORAGE_KEY_BASE}${Date.now()}`;
|
||||
}
|
||||
|
||||
module.exports = StorageSession;
|
||||
@ -1,105 +0,0 @@
|
||||
const MemorySession = require('./Memory');
|
||||
const AuthKey = require('../crypto/AuthKey');
|
||||
const BinaryReader = require('../extensions/BinaryReader');
|
||||
|
||||
const CURRENT_VERSION = '1';
|
||||
|
||||
class StringSession extends MemorySession {
|
||||
/**
|
||||
* This session file can be easily saved and loaded as a string. According
|
||||
* to the initial design, it contains only the data that is necessary for
|
||||
* successful connection and authentication, so takeout ID is not stored.
|
||||
|
||||
* It is thought to be used where you don't want to create any on-disk
|
||||
* files but would still like to be able to save and load existing sessions
|
||||
* by other means.
|
||||
|
||||
* You can use custom `encode` and `decode` functions, if present:
|
||||
|
||||
* `encode` definition must be ``function encode(value: Buffer) -> string:``.
|
||||
* `decode` definition must be ``function decode(value: string) -> Buffer:``.
|
||||
* @param session {string|null}
|
||||
*/
|
||||
constructor(session = undefined) {
|
||||
super();
|
||||
if (session) {
|
||||
if (session[0] !== CURRENT_VERSION) {
|
||||
throw new Error('Not a valid string');
|
||||
}
|
||||
session = session.slice(1);
|
||||
const r = StringSession.decode(session);
|
||||
const reader = new BinaryReader(r);
|
||||
this._dcId = reader.read(1)
|
||||
.readUInt8(0);
|
||||
const serverAddressLen = reader.read(2)
|
||||
.readInt16BE(0);
|
||||
this._serverAddress = String(reader.read(serverAddressLen));
|
||||
this._port = reader.read(2)
|
||||
.readInt16BE(0);
|
||||
this._key = reader.read(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x {Buffer}
|
||||
* @returns {string}
|
||||
*/
|
||||
static encode(x) {
|
||||
return x.toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param x {string}
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
static decode(x) {
|
||||
return Buffer.from(x, 'base64');
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (this._key) {
|
||||
this._authKey = new AuthKey();
|
||||
await this._authKey.setKey(this._key);
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
if (!this.authKey) {
|
||||
return '';
|
||||
}
|
||||
const dcBuffer = Buffer.from([this.dcId]);
|
||||
const addressBuffer = Buffer.from(this.serverAddress);
|
||||
const addressLengthBuffer = Buffer.alloc(2);
|
||||
addressLengthBuffer.writeInt16BE(addressBuffer.length, 0);
|
||||
const portBuffer = Buffer.alloc(2);
|
||||
portBuffer.writeInt16BE(this.port, 0);
|
||||
|
||||
return CURRENT_VERSION + StringSession.encode(Buffer.concat([
|
||||
dcBuffer,
|
||||
addressLengthBuffer,
|
||||
addressBuffer,
|
||||
portBuffer,
|
||||
this.authKey.getKey(),
|
||||
]));
|
||||
}
|
||||
|
||||
getAuthKey(dcId) {
|
||||
if (dcId && dcId !== this.dcId) {
|
||||
// Not supported.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.authKey;
|
||||
}
|
||||
|
||||
setAuthKey(authKey, dcId) {
|
||||
if (dcId && dcId !== this.dcId) {
|
||||
// Not supported.
|
||||
return;
|
||||
}
|
||||
|
||||
this.authKey = authKey;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StringSession;
|
||||
@ -1,15 +0,0 @@
|
||||
const Memory = require('./Memory');
|
||||
const StringSession = require('./StringSession');
|
||||
const CacheApiSession = require('./CacheApiSession');
|
||||
const LocalStorageSession = require('./LocalStorageSession');
|
||||
const IdbSession = require('./IdbSession');
|
||||
const CallbackSession = require('./CallbackSession');
|
||||
|
||||
module.exports = {
|
||||
Memory,
|
||||
StringSession,
|
||||
CacheApiSession,
|
||||
LocalStorageSession,
|
||||
IdbSession,
|
||||
CallbackSession,
|
||||
};
|
||||
7
src/lib/gramjs/sessions/index.ts
Normal file
7
src/lib/gramjs/sessions/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import CallbackSession from './CallbackSession';
|
||||
import MemorySession from './Memory';
|
||||
|
||||
export {
|
||||
CallbackSession,
|
||||
MemorySession,
|
||||
};
|
||||
@ -1,19 +0,0 @@
|
||||
const api = require('./api');
|
||||
|
||||
const LAYER = 197;
|
||||
const tlobjects = {};
|
||||
|
||||
for (const tl of Object.values(api)) {
|
||||
if (tl.CONSTRUCTOR_ID) {
|
||||
tlobjects[tl.CONSTRUCTOR_ID] = tl;
|
||||
} else {
|
||||
for (const sub of Object.values(tl)) {
|
||||
tlobjects[sub.CONSTRUCTOR_ID] = sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LAYER,
|
||||
tlobjects,
|
||||
};
|
||||
16
src/lib/gramjs/tl/AllTLObjects.ts
Normal file
16
src/lib/gramjs/tl/AllTLObjects.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Api } from '.';
|
||||
|
||||
const tlobjects: Record<number, any> = {};
|
||||
|
||||
for (const tl of Object.values(Api)) {
|
||||
if ('CONSTRUCTOR_ID' in tl) {
|
||||
tlobjects[tl.CONSTRUCTOR_ID] = tl;
|
||||
} else {
|
||||
for (const sub of Object.values(tl)) {
|
||||
tlobjects[sub.CONSTRUCTOR_ID] = sub;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const LAYER = 197;
|
||||
export { tlobjects };
|
||||
@ -1,48 +0,0 @@
|
||||
class MTProtoRequest {
|
||||
constructor() {
|
||||
this.sent = false;
|
||||
this.msgId = 0; // long
|
||||
this.sequence = 0;
|
||||
|
||||
this.dirty = false;
|
||||
this.sendTime = 0;
|
||||
this.confirmReceived = false;
|
||||
|
||||
// These should be overrode
|
||||
|
||||
this.constructorId = 0;
|
||||
this.confirmed = false;
|
||||
this.responded = false;
|
||||
}
|
||||
|
||||
// these should not be overrode
|
||||
onSendSuccess() {
|
||||
this.sendTime = Date.now();
|
||||
this.sent = true;
|
||||
}
|
||||
|
||||
onConfirm() {
|
||||
this.confirmReceived = true;
|
||||
}
|
||||
|
||||
needResend() {
|
||||
return this.dirty || (this.confirmed && !this.confirmReceived && Date.now() - this.sendTime > 3000);
|
||||
}
|
||||
|
||||
// These should be overrode
|
||||
onSend() {
|
||||
throw Error(`Not overload ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onResponse(buffer) {
|
||||
throw Error(`Not overload ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
onException(exception) {
|
||||
throw Error(`Not overload ${this.constructor.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MTProtoRequest;
|
||||
56
src/lib/gramjs/tl/MTProtoRequest.ts
Normal file
56
src/lib/gramjs/tl/MTProtoRequest.ts
Normal file
@ -0,0 +1,56 @@
|
||||
export abstract class MTProtoRequest {
|
||||
private sent: boolean;
|
||||
|
||||
private sequence: number;
|
||||
|
||||
private msgId: number;
|
||||
|
||||
private readonly dirty: boolean;
|
||||
|
||||
private sendTime: number;
|
||||
|
||||
private confirmReceived: boolean;
|
||||
|
||||
private constructorId: number;
|
||||
|
||||
private readonly confirmed: boolean;
|
||||
|
||||
private responded: boolean;
|
||||
|
||||
constructor() {
|
||||
this.sent = false;
|
||||
this.msgId = 0; // long
|
||||
this.sequence = 0;
|
||||
|
||||
this.dirty = false;
|
||||
this.sendTime = 0;
|
||||
this.confirmReceived = false;
|
||||
|
||||
// These should be overrode
|
||||
|
||||
this.constructorId = 0;
|
||||
this.confirmed = false;
|
||||
this.responded = false;
|
||||
}
|
||||
|
||||
// These should not be overrode
|
||||
onSendSuccess() {
|
||||
this.sendTime = new Date().getTime();
|
||||
this.sent = true;
|
||||
}
|
||||
|
||||
onConfirm() {
|
||||
this.confirmReceived = true;
|
||||
}
|
||||
|
||||
needResend() {
|
||||
return this.dirty || (this.confirmed && !this.confirmReceived && new Date().getTime() - this.sendTime > 3000);
|
||||
}
|
||||
|
||||
// These should be overrode
|
||||
abstract onSend(): void;
|
||||
|
||||
abstract onResponse(_buffer: Buffer): void;
|
||||
|
||||
abstract onException(_exception: Error): void;
|
||||
}
|
||||
8331
src/lib/gramjs/tl/api.d.ts
vendored
8331
src/lib/gramjs/tl/api.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -1,406 +1,5 @@
|
||||
const {
|
||||
parseTl,
|
||||
serializeBytes,
|
||||
serializeDate,
|
||||
} = require('./generationHelpers');
|
||||
const {
|
||||
toSignedLittleBuffer,
|
||||
} = require('../Helpers');
|
||||
import { buildApiFromTlSchema } from './apiHelpers';
|
||||
|
||||
const tlContent = require('./apiTl');
|
||||
const schemeContent = require('./schemaTl');
|
||||
const Api = buildApiFromTlSchema();
|
||||
|
||||
/* CONTEST
|
||||
const NAMED_AUTO_CASTS = new Set([
|
||||
'chatId,int'
|
||||
])
|
||||
const NAMED_BLACKLIST = new Set([
|
||||
'discardEncryption'
|
||||
])
|
||||
const AUTO_CASTS = new Set([
|
||||
'InputPeer',
|
||||
'InputChannel',
|
||||
'InputUser',
|
||||
'InputDialogPeer',
|
||||
'InputNotifyPeer',
|
||||
'InputMedia',
|
||||
'InputPhoto',
|
||||
'InputMessage',
|
||||
'InputDocument',
|
||||
'InputChatPhoto'
|
||||
])
|
||||
|
||||
*/
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const CACHING_SUPPORTED = typeof self !== 'undefined' && self.localStorage !== undefined;
|
||||
|
||||
const CACHE_KEY = 'GramJs:apiCache';
|
||||
|
||||
function buildApiFromTlSchema() {
|
||||
let definitions;
|
||||
const fromCache = CACHING_SUPPORTED && loadFromCache();
|
||||
|
||||
if (fromCache) {
|
||||
definitions = fromCache;
|
||||
} else {
|
||||
definitions = loadFromTlSchemas();
|
||||
|
||||
if (CACHING_SUPPORTED) {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(definitions));
|
||||
}
|
||||
}
|
||||
|
||||
return mergeWithNamespaces(
|
||||
createClasses('constructor', definitions.constructors),
|
||||
createClasses('request', definitions.requests),
|
||||
);
|
||||
}
|
||||
|
||||
function loadFromCache() {
|
||||
const jsonCache = localStorage.getItem(CACHE_KEY);
|
||||
return jsonCache && JSON.parse(jsonCache);
|
||||
}
|
||||
|
||||
function loadFromTlSchemas() {
|
||||
const [constructorParamsApi, functionParamsApi] = extractParams(tlContent);
|
||||
const [constructorParamsSchema, functionParamsSchema] = extractParams(schemeContent);
|
||||
const constructors = [].concat(constructorParamsApi, constructorParamsSchema);
|
||||
const requests = [].concat(functionParamsApi, functionParamsSchema);
|
||||
|
||||
return {
|
||||
constructors,
|
||||
requests,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeWithNamespaces(obj1, obj2) {
|
||||
const result = { ...obj1 };
|
||||
|
||||
Object.keys(obj2)
|
||||
.forEach((key) => {
|
||||
if (typeof obj2[key] === 'function' || !result[key]) {
|
||||
result[key] = obj2[key];
|
||||
} else {
|
||||
Object.assign(result[key], obj2[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractParams(fileContent) {
|
||||
const f = parseTl(fileContent);
|
||||
const constructors = [];
|
||||
const functions = [];
|
||||
for (const d of f) {
|
||||
if (d.isFunction) {
|
||||
functions.push(d);
|
||||
} else {
|
||||
constructors.push(d);
|
||||
}
|
||||
}
|
||||
return [constructors, functions];
|
||||
}
|
||||
|
||||
function argToBytes(x, type) {
|
||||
switch (type) {
|
||||
case 'int': {
|
||||
const i = Buffer.alloc(4);
|
||||
i.writeInt32LE(x, 0);
|
||||
return i;
|
||||
}
|
||||
case 'long':
|
||||
return toSignedLittleBuffer(x, 8);
|
||||
case 'int128':
|
||||
return toSignedLittleBuffer(x, 16);
|
||||
case 'int256':
|
||||
return toSignedLittleBuffer(x, 32);
|
||||
case 'double': {
|
||||
const d = Buffer.alloc(8);
|
||||
d.writeDoubleLE(x, 0);
|
||||
return d;
|
||||
}
|
||||
case 'string':
|
||||
return serializeBytes(x);
|
||||
case 'Bool':
|
||||
return x ? Buffer.from('b5757299', 'hex') : Buffer.from('379779bc', 'hex');
|
||||
case 'true':
|
||||
return Buffer.alloc(0);
|
||||
case 'bytes':
|
||||
return serializeBytes(x);
|
||||
case 'date':
|
||||
return serializeDate(x);
|
||||
default:
|
||||
return x.getBytes();
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
CONTEST
|
||||
async function getInputFromResolve(utils, client, peer, peerType) {
|
||||
switch (peerType) {
|
||||
case 'InputPeer':
|
||||
return utils.getInputPeer(await client.getInputEntity(peer))
|
||||
case 'InputChannel':
|
||||
return utils.getInputChannel(await client.getInputEntity(peer))
|
||||
case 'InputUser':
|
||||
return utils.getInputUser(await client.getInputEntity(peer))
|
||||
case 'InputDialogPeer':
|
||||
return await client._getInputDialog(peer)
|
||||
case 'InputNotifyPeer':
|
||||
return await client._getInputNotify(peer)
|
||||
case 'InputMedia':
|
||||
return utils.getInputMedia(peer)
|
||||
case 'InputPhoto':
|
||||
return utils.getInputPhoto(peer)
|
||||
case 'InputMessage':
|
||||
return utils.getInputMessage(peer)
|
||||
case 'InputDocument':
|
||||
return utils.getInputDocument(peer)
|
||||
case 'InputChatPhoto':
|
||||
return utils.getInputChatPhoto(peer)
|
||||
case 'chatId,int' :
|
||||
return await client.getPeerId(peer, false)
|
||||
default:
|
||||
throw new Error('unsupported peer type : ' + peerType)
|
||||
}
|
||||
}
|
||||
*/
|
||||
function getArgFromReader(reader, arg) {
|
||||
if (arg.isVector) {
|
||||
if (arg.useVectorId) {
|
||||
reader.readInt();
|
||||
}
|
||||
const temp = [];
|
||||
const len = reader.readInt();
|
||||
arg.isVector = false;
|
||||
for (let i = 0; i < len; i++) {
|
||||
temp.push(getArgFromReader(reader, arg));
|
||||
}
|
||||
arg.isVector = true;
|
||||
return temp;
|
||||
} else if (arg.flagIndicator) {
|
||||
return reader.readInt();
|
||||
} else {
|
||||
switch (arg.type) {
|
||||
case 'int':
|
||||
return reader.readInt();
|
||||
case 'long':
|
||||
return reader.readLong();
|
||||
case 'int128':
|
||||
return reader.readLargeInt(128);
|
||||
case 'int256':
|
||||
return reader.readLargeInt(256);
|
||||
case 'double':
|
||||
return reader.readDouble();
|
||||
case 'string':
|
||||
return reader.tgReadString();
|
||||
case 'Bool':
|
||||
return reader.tgReadBool();
|
||||
case 'true':
|
||||
return true;
|
||||
case 'bytes':
|
||||
return reader.tgReadBytes();
|
||||
case 'date':
|
||||
return reader.tgReadDate();
|
||||
default:
|
||||
if (!arg.skipConstructorId) {
|
||||
return reader.tgReadObject();
|
||||
} else {
|
||||
throw new Error(`Unknown type ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createClasses(classesType, params) {
|
||||
const classes = {};
|
||||
for (const classParams of params) {
|
||||
const {
|
||||
name,
|
||||
constructorId,
|
||||
subclassOfId,
|
||||
argsConfig,
|
||||
namespace,
|
||||
result,
|
||||
} = classParams;
|
||||
const fullName = [namespace, name].join('.')
|
||||
.replace(/^\./, '');
|
||||
|
||||
class VirtualClass {
|
||||
static CONSTRUCTOR_ID = constructorId;
|
||||
|
||||
static SUBCLASS_OF_ID = subclassOfId;
|
||||
|
||||
static className = fullName;
|
||||
|
||||
static classType = classesType;
|
||||
|
||||
CONSTRUCTOR_ID = constructorId;
|
||||
|
||||
SUBCLASS_OF_ID = subclassOfId;
|
||||
|
||||
className = fullName;
|
||||
|
||||
classType = classesType;
|
||||
|
||||
constructor(args) {
|
||||
args = args || {};
|
||||
Object.keys(args)
|
||||
.forEach((argName) => {
|
||||
this[argName] = args[argName];
|
||||
});
|
||||
}
|
||||
|
||||
static fromReader(reader) {
|
||||
const args = {};
|
||||
|
||||
for (const argName in argsConfig) {
|
||||
if (argsConfig.hasOwnProperty(argName)) {
|
||||
const arg = argsConfig[argName];
|
||||
if (arg.isFlag) {
|
||||
const flagGroupSuffix = arg.flagGroup > 1 ? arg.flagGroup : '';
|
||||
const flagValue = args[`flags${flagGroupSuffix}`] & (1 << arg.flagIndex);
|
||||
if (arg.type === 'true') {
|
||||
args[argName] = Boolean(flagValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
args[argName] = flagValue ? getArgFromReader(reader, arg) : undefined;
|
||||
} else {
|
||||
args[argName] = getArgFromReader(reader, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VirtualClass(args);
|
||||
}
|
||||
|
||||
getBytes() {
|
||||
// The next is pseudo-code:
|
||||
const idForBytes = this.CONSTRUCTOR_ID;
|
||||
const c = Buffer.alloc(4);
|
||||
c.writeUInt32LE(idForBytes, 0);
|
||||
const buffers = [c];
|
||||
for (const arg in argsConfig) {
|
||||
if (argsConfig.hasOwnProperty(arg)) {
|
||||
if (argsConfig[arg].isFlag) {
|
||||
if ((this[arg] === false && argsConfig[arg].type === 'true')
|
||||
|| this[arg] === undefined) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (argsConfig[arg].isVector) {
|
||||
if (argsConfig[arg].useVectorId) {
|
||||
buffers.push(Buffer.from('15c4b51c', 'hex'));
|
||||
}
|
||||
const l = Buffer.alloc(4);
|
||||
l.writeInt32LE(this[arg].length, 0);
|
||||
buffers.push(l, Buffer.concat(this[arg].map((x) => argToBytes(x, argsConfig[arg].type))));
|
||||
} else if (argsConfig[arg].flagIndicator) {
|
||||
if (!Object.values(argsConfig)
|
||||
.some((f) => f.isFlag)) {
|
||||
buffers.push(Buffer.alloc(4));
|
||||
} else {
|
||||
let flagCalculate = 0;
|
||||
for (const f in argsConfig) {
|
||||
if (argsConfig[f].isFlag) {
|
||||
if ((this[f] === false && argsConfig[f].type === 'true')
|
||||
|| this[f] === undefined) {
|
||||
flagCalculate |= 0;
|
||||
} else {
|
||||
flagCalculate |= 1 << argsConfig[f].flagIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
const f = Buffer.alloc(4);
|
||||
f.writeUInt32LE(flagCalculate, 0);
|
||||
buffers.push(f);
|
||||
}
|
||||
} else {
|
||||
buffers.push(argToBytes(this[arg], argsConfig[arg].type));
|
||||
|
||||
if (this[arg] && typeof this[arg].getBytes === 'function') {
|
||||
let boxed = (argsConfig[arg].type.charAt(argsConfig[arg].type.indexOf('.') + 1));
|
||||
boxed = boxed === boxed.toUpperCase();
|
||||
if (!boxed) {
|
||||
buffers.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Buffer.concat(buffers);
|
||||
}
|
||||
|
||||
readResult(reader) {
|
||||
if (classesType !== 'request') {
|
||||
throw new Error('`readResult()` called for non-request instance');
|
||||
}
|
||||
|
||||
const m = result.match(/Vector<(int|long)>/);
|
||||
if (m) {
|
||||
reader.readInt();
|
||||
const temp = [];
|
||||
const len = reader.readInt();
|
||||
if (m[1] === 'int') {
|
||||
for (let i = 0; i < len; i++) {
|
||||
temp.push(reader.readInt());
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < len; i++) {
|
||||
temp.push(reader.readLong());
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
} else {
|
||||
return reader.tgReadObject();
|
||||
}
|
||||
}
|
||||
|
||||
/* CONTEST
|
||||
async resolve(client, utils) {
|
||||
|
||||
if (classesType !== 'request') {
|
||||
throw new Error('`resolve()` called for non-request instance')
|
||||
}
|
||||
|
||||
for (const arg in argsConfig) {
|
||||
if (argsConfig.hasOwnProperty(arg)) {
|
||||
if (!AUTO_CASTS.has(argsConfig[arg].type)) {
|
||||
if (!NAMED_AUTO_CASTS.has(`${argsConfig[arg].name},${argsConfig[arg].type}`)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (argsConfig[arg].isFlag) {
|
||||
if (!this[arg]) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (argsConfig[arg].isVector) {
|
||||
const temp = []
|
||||
for (const x of this[arg]) {
|
||||
temp.push(await getInputFromResolve(utils, client, x, argsConfig[arg].type))
|
||||
}
|
||||
this[arg] = temp
|
||||
} else {
|
||||
this[arg] = await getInputFromResolve(utils, client, this[arg], argsConfig[arg].type)
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
if (namespace) {
|
||||
if (!classes[namespace]) {
|
||||
classes[namespace] = {};
|
||||
}
|
||||
classes[namespace][name] = VirtualClass;
|
||||
} else {
|
||||
classes[name] = VirtualClass;
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
module.exports = buildApiFromTlSchema();
|
||||
export default Api;
|
||||
|
||||
323
src/lib/gramjs/tl/apiHelpers.ts
Normal file
323
src/lib/gramjs/tl/apiHelpers.ts
Normal file
@ -0,0 +1,323 @@
|
||||
import type { BinaryReader } from '../extensions';
|
||||
|
||||
import tlContent from './apiTl';
|
||||
import {
|
||||
type GenerationArgConfig, type GenerationEntryConfig, parseTl, serializeBytes, serializeDate,
|
||||
} from './generationHelpers';
|
||||
import schemeContent from './schemaTl';
|
||||
|
||||
import { toSignedLittleBuffer } from '../Helpers';
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const CACHING_SUPPORTED = typeof self !== 'undefined' && self.localStorage !== undefined;
|
||||
|
||||
const CACHE_KEY = 'GramJs:apiCache';
|
||||
|
||||
type UnsaveVirtualClass = Record<string, any>;
|
||||
|
||||
export function buildApiFromTlSchema() {
|
||||
let definitions;
|
||||
const fromCache = CACHING_SUPPORTED && loadFromCache();
|
||||
|
||||
if (fromCache) {
|
||||
definitions = fromCache;
|
||||
} else {
|
||||
definitions = loadFromTlSchemas();
|
||||
|
||||
if (CACHING_SUPPORTED) {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(definitions));
|
||||
}
|
||||
}
|
||||
|
||||
return mergeWithNamespaces(
|
||||
createClasses('constructor', definitions.constructors),
|
||||
createClasses('request', definitions.requests),
|
||||
);
|
||||
}
|
||||
|
||||
function loadFromCache(): { constructors: GenerationEntryConfig[]; requests: GenerationEntryConfig[] } {
|
||||
const jsonCache = localStorage.getItem(CACHE_KEY);
|
||||
return jsonCache && JSON.parse(jsonCache);
|
||||
}
|
||||
|
||||
function loadFromTlSchemas() {
|
||||
const [constructorParamsApi, functionParamsApi] = extractParams(tlContent);
|
||||
const [constructorParamsSchema, functionParamsSchema] = extractParams(schemeContent);
|
||||
const constructors = ([] as GenerationEntryConfig[]).concat(constructorParamsApi, constructorParamsSchema);
|
||||
const requests = ([] as GenerationEntryConfig[]).concat(functionParamsApi, functionParamsSchema);
|
||||
|
||||
return {
|
||||
constructors,
|
||||
requests,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeWithNamespaces<T extends unknown>(obj1: Record<string, T>, obj2: Record<string, T>): Record<string, T> {
|
||||
const result: Record<string, any> = { ...obj1 };
|
||||
|
||||
Object.keys(obj2)
|
||||
.forEach((key) => {
|
||||
if (typeof obj2[key] === 'function' || !result[key]) {
|
||||
result[key] = obj2[key];
|
||||
} else {
|
||||
Object.assign(result[key], obj2[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function extractParams(fileContent: string) {
|
||||
const f = parseTl(fileContent);
|
||||
const constructors = [];
|
||||
const functions = [];
|
||||
for (const d of f) {
|
||||
if (d.isFunction) {
|
||||
functions.push(d);
|
||||
} else {
|
||||
constructors.push(d);
|
||||
}
|
||||
}
|
||||
return [constructors, functions];
|
||||
}
|
||||
|
||||
function argToBytes(x: any, type: string) {
|
||||
switch (type) {
|
||||
case 'int': {
|
||||
const i = Buffer.alloc(4);
|
||||
i.writeInt32LE(x, 0);
|
||||
return i;
|
||||
}
|
||||
case 'long':
|
||||
return toSignedLittleBuffer(x, 8);
|
||||
case 'int128':
|
||||
return toSignedLittleBuffer(x, 16);
|
||||
case 'int256':
|
||||
return toSignedLittleBuffer(x, 32);
|
||||
case 'double': {
|
||||
const d = Buffer.alloc(8);
|
||||
d.writeDoubleLE(x, 0);
|
||||
return d;
|
||||
}
|
||||
case 'string':
|
||||
return serializeBytes(x);
|
||||
case 'Bool':
|
||||
return x ? Buffer.from('b5757299', 'hex') : Buffer.from('379779bc', 'hex');
|
||||
case 'true':
|
||||
return Buffer.alloc(0);
|
||||
case 'bytes':
|
||||
return serializeBytes(x);
|
||||
case 'date':
|
||||
return serializeDate(x);
|
||||
default:
|
||||
return x.getBytes();
|
||||
}
|
||||
}
|
||||
|
||||
function getArgFromReader(reader: BinaryReader, arg: GenerationArgConfig): any {
|
||||
if (arg.isVector) {
|
||||
if (arg.useVectorId) {
|
||||
reader.readInt();
|
||||
}
|
||||
const temp = [];
|
||||
const len = reader.readInt();
|
||||
arg.isVector = false;
|
||||
for (let i = 0; i < len; i++) {
|
||||
temp.push(getArgFromReader(reader, arg));
|
||||
}
|
||||
arg.isVector = true;
|
||||
return temp;
|
||||
} else if (arg.flagIndicator) {
|
||||
return reader.readInt();
|
||||
} else {
|
||||
switch (arg.type) {
|
||||
case 'int':
|
||||
return reader.readInt();
|
||||
case 'long':
|
||||
return reader.readLong();
|
||||
case 'int128':
|
||||
return reader.readLargeInt(128);
|
||||
case 'int256':
|
||||
return reader.readLargeInt(256);
|
||||
case 'double':
|
||||
return reader.readDouble();
|
||||
case 'string':
|
||||
return reader.tgReadString();
|
||||
case 'Bool':
|
||||
return reader.tgReadBool();
|
||||
case 'true':
|
||||
return true;
|
||||
case 'bytes':
|
||||
return reader.tgReadBytes();
|
||||
case 'date':
|
||||
return reader.tgReadDate();
|
||||
default:
|
||||
if (!arg.skipConstructorId) {
|
||||
return reader.tgReadObject();
|
||||
} else {
|
||||
throw new Error(`Unknown type ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createClasses(classesType: 'constructor' | 'request', params: GenerationEntryConfig[]) {
|
||||
const classes: Record<string, any> = {};
|
||||
for (const classParams of params) {
|
||||
const {
|
||||
name,
|
||||
constructorId,
|
||||
subclassOfId,
|
||||
argsConfig,
|
||||
namespace,
|
||||
result,
|
||||
} = classParams;
|
||||
const fullName = [namespace, name].join('.')
|
||||
.replace(/^\./, '');
|
||||
|
||||
class VirtualClass {
|
||||
static CONSTRUCTOR_ID = constructorId;
|
||||
|
||||
static SUBCLASS_OF_ID = subclassOfId;
|
||||
|
||||
static className = fullName;
|
||||
|
||||
static classType = classesType;
|
||||
|
||||
CONSTRUCTOR_ID = constructorId;
|
||||
|
||||
SUBCLASS_OF_ID = subclassOfId;
|
||||
|
||||
className = fullName;
|
||||
|
||||
classType = classesType;
|
||||
|
||||
constructor(args: Record<string, any>) {
|
||||
args = args || {};
|
||||
Object.keys(args)
|
||||
.forEach((argName) => {
|
||||
(this as UnsaveVirtualClass)[argName] = args[argName];
|
||||
});
|
||||
}
|
||||
|
||||
static fromReader(reader: BinaryReader) {
|
||||
const args: Record<string, any> = {};
|
||||
|
||||
for (const argName in argsConfig) {
|
||||
if (argsConfig.hasOwnProperty(argName)) {
|
||||
const arg = argsConfig[argName];
|
||||
if (arg.isFlag) {
|
||||
const flagGroupSuffix = arg.flagGroup > 1 ? arg.flagGroup : '';
|
||||
const flagValue = args[`flags${flagGroupSuffix}`] & (1 << arg.flagIndex);
|
||||
if (arg.type === 'true') {
|
||||
args[argName] = Boolean(flagValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
args[argName] = flagValue ? getArgFromReader(reader, arg) : undefined;
|
||||
} else {
|
||||
args[argName] = getArgFromReader(reader, arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VirtualClass(args);
|
||||
}
|
||||
|
||||
getBytes() {
|
||||
// The next is pseudo-code:
|
||||
const idForBytes = this.CONSTRUCTOR_ID;
|
||||
const c = Buffer.alloc(4);
|
||||
c.writeUInt32LE(idForBytes, 0);
|
||||
const buffers = [c];
|
||||
for (const arg in argsConfig) {
|
||||
if (argsConfig.hasOwnProperty(arg)) {
|
||||
if (argsConfig[arg].isFlag) {
|
||||
if (((this as UnsaveVirtualClass)[arg] === false && argsConfig[arg].type === 'true')
|
||||
|| (this as UnsaveVirtualClass)[arg] === undefined) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (argsConfig[arg].isVector) {
|
||||
if (argsConfig[arg].useVectorId) {
|
||||
buffers.push(Buffer.from('15c4b51c', 'hex'));
|
||||
}
|
||||
const l = Buffer.alloc(4);
|
||||
l.writeInt32LE((this as UnsaveVirtualClass)[arg].length, 0);
|
||||
buffers.push(l, Buffer.concat((this as UnsaveVirtualClass)[arg].map((x: any) => (
|
||||
argToBytes(x, argsConfig[arg].type)
|
||||
))));
|
||||
} else if (argsConfig[arg].flagIndicator) {
|
||||
if (!Object.values(argsConfig)
|
||||
.some((f) => f.isFlag)) {
|
||||
buffers.push(Buffer.alloc(4));
|
||||
} else {
|
||||
let flagCalculate = 0;
|
||||
for (const f in argsConfig) {
|
||||
if (argsConfig[f].isFlag) {
|
||||
if (((this as UnsaveVirtualClass)[f] === false && argsConfig[f].type === 'true')
|
||||
|| (this as UnsaveVirtualClass)[f] === undefined) {
|
||||
flagCalculate |= 0;
|
||||
} else {
|
||||
flagCalculate |= 1 << argsConfig[f].flagIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
const f = Buffer.alloc(4);
|
||||
f.writeUInt32LE(flagCalculate, 0);
|
||||
buffers.push(f);
|
||||
}
|
||||
} else {
|
||||
buffers.push(argToBytes((this as UnsaveVirtualClass)[arg], argsConfig[arg].type));
|
||||
|
||||
if ((this as UnsaveVirtualClass)[arg]
|
||||
&& typeof (this as UnsaveVirtualClass)[arg].getBytes === 'function') {
|
||||
const firstChar = (argsConfig[arg].type.charAt(argsConfig[arg].type.indexOf('.') + 1));
|
||||
const boxed = firstChar === firstChar.toUpperCase();
|
||||
if (!boxed) {
|
||||
buffers.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Buffer.concat(buffers);
|
||||
}
|
||||
|
||||
readResult(reader: BinaryReader) {
|
||||
if (classesType !== 'request') {
|
||||
throw new Error('`readResult()` called for non-request instance');
|
||||
}
|
||||
|
||||
const m = result.match(/Vector<(int|long)>/);
|
||||
if (m) {
|
||||
reader.readInt();
|
||||
const temp = [];
|
||||
const len = reader.readInt();
|
||||
if (m[1] === 'int') {
|
||||
for (let i = 0; i < len; i++) {
|
||||
temp.push(reader.readInt());
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < len; i++) {
|
||||
temp.push(reader.readLong());
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
} else {
|
||||
return reader.tgReadObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (namespace) {
|
||||
if (!classes[namespace]) {
|
||||
classes[namespace] = {};
|
||||
}
|
||||
classes[namespace][name] = VirtualClass;
|
||||
} else {
|
||||
classes[name] = VirtualClass;
|
||||
}
|
||||
}
|
||||
|
||||
return classes;
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
module.exports = `boolFalse#bc799737 = Bool;
|
||||
export default `boolFalse#bc799737 = Bool;
|
||||
boolTrue#997275b5 = Bool;
|
||||
true#3fedd339 = True;
|
||||
vector#1cb5c415 {t:Type} # [ t ] = Vector t;
|
||||
@ -1,22 +1,29 @@
|
||||
const { inflate } = require('pako/dist/pako_inflate');
|
||||
const { serializeBytes } = require('../index');
|
||||
import { inflate } from 'pako/dist/pako_inflate';
|
||||
|
||||
// CONTEST const { deflate } = require('pako/dist/pako_deflate')
|
||||
import type { BinaryReader } from '../../extensions';
|
||||
|
||||
class GZIPPacked {
|
||||
import { serializeBytes } from '..';
|
||||
|
||||
export default class GZIPPacked {
|
||||
static CONSTRUCTOR_ID = 0x3072cfa1;
|
||||
|
||||
static classType = 'constructor';
|
||||
|
||||
constructor(data) {
|
||||
data: Buffer;
|
||||
|
||||
private CONSTRUCTOR_ID: number;
|
||||
|
||||
private classType: string;
|
||||
|
||||
constructor(data: Buffer) {
|
||||
this.data = data;
|
||||
this.CONSTRUCTOR_ID = 0x3072cfa1;
|
||||
this.classType = 'constructor';
|
||||
}
|
||||
|
||||
static async gzipIfSmaller(contentRelated, data) {
|
||||
static async gzipIfSmaller(contentRelated: boolean, data: Buffer) {
|
||||
if (contentRelated && data.length > 512) {
|
||||
const gzipped = await (new GZIPPacked(data)).toBytes();
|
||||
const gzipped = await new GZIPPacked(data).toBytes();
|
||||
if (gzipped.length < data.length) {
|
||||
return gzipped;
|
||||
}
|
||||
@ -24,28 +31,16 @@ class GZIPPacked {
|
||||
return data;
|
||||
}
|
||||
|
||||
static gzip(input) {
|
||||
static gzip(input: Buffer) {
|
||||
return Buffer.from(input);
|
||||
// TODO this usually makes it faster for large requests
|
||||
// return Buffer.from(deflate(input, { level: 9, gzip: true }))
|
||||
}
|
||||
|
||||
static ungzip(input) {
|
||||
static ungzip(input: Buffer) {
|
||||
return Buffer.from(inflate(input));
|
||||
}
|
||||
|
||||
static read(reader) {
|
||||
const constructor = reader.readInt(false);
|
||||
if (constructor !== GZIPPacked.CONSTRUCTOR_ID) {
|
||||
throw new Error('not equal');
|
||||
}
|
||||
return GZIPPacked.gzip(reader.tgReadBytes());
|
||||
}
|
||||
|
||||
static async fromReader(reader) {
|
||||
return new GZIPPacked(await GZIPPacked.ungzip(reader.tgReadBytes()));
|
||||
}
|
||||
|
||||
async toBytes() {
|
||||
const g = Buffer.alloc(4);
|
||||
g.writeUInt32LE(GZIPPacked.CONSTRUCTOR_ID, 0);
|
||||
@ -54,6 +49,17 @@ class GZIPPacked {
|
||||
serializeBytes(await GZIPPacked.gzip(this.data)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GZIPPacked;
|
||||
static read(reader: BinaryReader) {
|
||||
const constructor = reader.readInt(false);
|
||||
if (constructor !== GZIPPacked.CONSTRUCTOR_ID) {
|
||||
throw new Error('not equal');
|
||||
}
|
||||
return GZIPPacked.gzip(reader.tgReadBytes());
|
||||
}
|
||||
|
||||
static async fromReader(reader: BinaryReader) {
|
||||
const data = reader.tgReadBytes();
|
||||
return new GZIPPacked(await GZIPPacked.ungzip(data));
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
const TLMessage = require('./TLMessage');
|
||||
import type { BinaryReader } from '../../extensions';
|
||||
|
||||
class MessageContainer {
|
||||
import TLMessage from './TLMessage';
|
||||
|
||||
export default class MessageContainer {
|
||||
static CONSTRUCTOR_ID = 0x73f1f8dc;
|
||||
|
||||
static classType = 'constructor';
|
||||
@ -20,27 +22,31 @@ class MessageContainer {
|
||||
// other factors like size per request, but we cannot know this.
|
||||
static MAXIMUM_LENGTH = 100;
|
||||
|
||||
constructor(messages) {
|
||||
private CONSTRUCTOR_ID: number;
|
||||
|
||||
private messages: any[];
|
||||
|
||||
private classType: string;
|
||||
|
||||
constructor(messages: any[]) {
|
||||
this.CONSTRUCTOR_ID = 0x73f1f8dc;
|
||||
this.messages = messages;
|
||||
this.classType = 'constructor';
|
||||
}
|
||||
|
||||
static fromReader(reader) {
|
||||
static fromReader(reader: BinaryReader) {
|
||||
const messages = [];
|
||||
const length = reader.readInt();
|
||||
for (let x = 0; x < length; x++) {
|
||||
const totalLength = reader.readInt();
|
||||
for (let x = 0; x < totalLength; x++) {
|
||||
const msgId = reader.readLong();
|
||||
const seqNo = reader.readInt();
|
||||
const containerLength = reader.readInt();
|
||||
const length = reader.readInt();
|
||||
const before = reader.tellPosition();
|
||||
const obj = reader.tgReadObject();
|
||||
reader.setPosition(before + containerLength);
|
||||
reader.setPosition(before + length);
|
||||
const tlMessage = new TLMessage(msgId, seqNo, obj);
|
||||
messages.push(tlMessage);
|
||||
}
|
||||
return new MessageContainer(messages);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MessageContainer;
|
||||
@ -1,34 +0,0 @@
|
||||
const { RpcError } = require('../index').constructors;
|
||||
const GZIPPacked = require('./GZIPPacked');
|
||||
|
||||
class RPCResult {
|
||||
static CONSTRUCTOR_ID = 0xf35c6d01;
|
||||
|
||||
static classType = 'constructor';
|
||||
|
||||
constructor(reqMsgId, body, error) {
|
||||
this.CONSTRUCTOR_ID = 0xf35c6d01;
|
||||
this.reqMsgId = reqMsgId;
|
||||
this.body = body;
|
||||
this.error = error;
|
||||
this.classType = 'constructor';
|
||||
}
|
||||
|
||||
static async fromReader(reader) {
|
||||
const msgId = reader.readLong();
|
||||
const innerCode = reader.readInt(false);
|
||||
if (innerCode === RpcError.CONSTRUCTOR_ID) {
|
||||
return new RPCResult(msgId, undefined, RpcError.fromReader(reader));
|
||||
}
|
||||
if (innerCode === GZIPPacked.CONSTRUCTOR_ID) {
|
||||
return new RPCResult(msgId, (await GZIPPacked.fromReader(reader)).data);
|
||||
}
|
||||
reader.seek(-4);
|
||||
// This reader.read() will read more than necessary, but it's okay.
|
||||
// We could make use of MessageContainer's length here, but since
|
||||
// it's not necessary we don't need to care about it.
|
||||
return new RPCResult(msgId, reader.read(), undefined);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RPCResult;
|
||||
58
src/lib/gramjs/tl/core/RPCResult.ts
Normal file
58
src/lib/gramjs/tl/core/RPCResult.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import type BigInt from 'big-integer';
|
||||
|
||||
import type { BinaryReader } from '../../extensions';
|
||||
|
||||
import Api from '../api';
|
||||
|
||||
import GZIPPacked from './GZIPPacked';
|
||||
|
||||
export default class RPCResult {
|
||||
static CONSTRUCTOR_ID = 0xf35c6d01;
|
||||
|
||||
static classType = 'constructor';
|
||||
|
||||
private CONSTRUCTOR_ID: number;
|
||||
|
||||
private reqMsgId: BigInt.BigInteger;
|
||||
|
||||
private body?: Buffer;
|
||||
|
||||
private error?: Api.RpcError;
|
||||
|
||||
private classType: string;
|
||||
|
||||
constructor(
|
||||
reqMsgId: BigInt.BigInteger,
|
||||
body?: Buffer,
|
||||
error?: Api.RpcError,
|
||||
) {
|
||||
this.CONSTRUCTOR_ID = 0xf35c6d01;
|
||||
this.reqMsgId = reqMsgId;
|
||||
this.body = body;
|
||||
this.error = error;
|
||||
this.classType = 'constructor';
|
||||
}
|
||||
|
||||
static async fromReader(reader: BinaryReader) {
|
||||
const msgId = reader.readLong();
|
||||
const innerCode = reader.readInt(false);
|
||||
if (innerCode === Api.RpcError.CONSTRUCTOR_ID) {
|
||||
return new RPCResult(
|
||||
msgId,
|
||||
undefined,
|
||||
Api.RpcError.fromReader(reader),
|
||||
);
|
||||
}
|
||||
if (innerCode === GZIPPacked.CONSTRUCTOR_ID) {
|
||||
return new RPCResult(
|
||||
msgId,
|
||||
(await GZIPPacked.fromReader(reader)).data,
|
||||
);
|
||||
}
|
||||
reader.seek(-4);
|
||||
// This reader.read() will read more than necessary, but it's okay.
|
||||
// We could make use of MessageContainer's length here, but since
|
||||
// it's not necessary we don't need to care about it.
|
||||
return new RPCResult(msgId, reader.read(), undefined);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user