Migrate GramJS to TypeScript (#5364)

This commit is contained in:
zubiden 2025-01-21 18:20:57 +01:00 committed by Alexander Zinchuk
parent 5381c148e4
commit d1304c621d
114 changed files with 8267 additions and 7710 deletions

View File

@ -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",

View File

@ -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),
};
}

View File

@ -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) => {

View File

@ -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) {

View File

@ -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,
},
});
}

View File

@ -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',

View File

@ -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);

View File

@ -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,
};

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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';

View File

@ -24,7 +24,7 @@ export interface ApiPhotoSize extends ApiDimensions {
export interface ApiVideoSize extends ApiDimensions {
type: 'u' | 'v';
videoStartTs: number;
videoStartTs?: number;
size: number;
}

View File

@ -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 = {

View File

@ -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 });

View File

@ -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({

View File

@ -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

View File

@ -3,6 +3,7 @@
"plugins": [
"@typescript-eslint"
],
"root": true,
"rules": {
"indent": [
"error",

View File

@ -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
View 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;
}

View File

@ -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,
};

View File

@ -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
View 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
}
} */
}

View File

@ -1 +0,0 @@
module.exports = '0.0.2';

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View 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));
}
}

View File

@ -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;

View File

@ -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;

View 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 };

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
};
]);

View File

@ -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,
};

View 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';

View File

@ -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
*/

View File

@ -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;

View File

@ -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,
};

View File

@ -1,12 +0,0 @@
const NewMessage = require('./NewMessage');
const Raw = require('./Raw');
class StopPropagation extends Error {
}
module.exports = {
NewMessage,
StopPropagation,
Raw,
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,
};

View 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,
};

View File

@ -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
View 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,
}

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;
});
}
}

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
// }

View File

@ -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,
};

View File

@ -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,
};

View File

@ -0,0 +1,3 @@
export { Connection, HttpConnection } from './Connection';
export { ConnectionTCPAbridged } from './TCPAbridged';
export { ConnectionTCPObfuscated } from './TCPObfuscated';

View File

@ -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,
};

View 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,
};

View File

@ -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,
};

View 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;
}
}

View File

@ -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;

View 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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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() {}
}

View File

@ -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;

View File

@ -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;

View File

@ -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,
};

View File

@ -0,0 +1,7 @@
import CallbackSession from './CallbackSession';
import MemorySession from './Memory';
export {
CallbackSession,
MemorySession,
};

View File

@ -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,
};

View 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 };

View File

@ -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;

View 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;
}

File diff suppressed because it is too large Load Diff

View File

@ -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;

View 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;
}

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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;

View 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