dograma
Version:
NodeJS/Browser MTProto API Telegram client library,
544 lines (501 loc) • 14.6 kB
text/typescript
import bigInt from "big-integer";
import type { EntityLike } from "./define";
import type { Api } from "./tl";
import crypto from "./CryptoFile";
/**
* converts a buffer to big int
* @param buffer
* @param little
* @param signed
* @returns {bigInt.BigInteger}
*/
export function readBigIntFromBuffer(
buffer: Buffer,
little = true,
signed = false
): bigInt.BigInteger {
let randBuffer = Buffer.from(buffer);
const bytesNumber = randBuffer.length;
if (little) {
randBuffer = randBuffer.reverse();
}
let bigIntVar = bigInt(randBuffer.toString("hex"), 16) as bigInt.BigInteger;
if (signed && Math.floor(bigIntVar.toString(2).length / 8) >= bytesNumber) {
bigIntVar = bigIntVar.subtract(bigInt(2).pow(bigInt(bytesNumber * 8)));
}
return bigIntVar;
}
export function generateRandomBigInt() {
return readBigIntFromBuffer(generateRandomBytes(8), false);
}
export function escapeRegex(string: string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
}
export function groupBy(list: any[], keyGetter: Function) {
const map = new Map();
list.forEach((item) => {
const key = keyGetter(item);
const collection = map.get(key);
if (!collection) {
map.set(key, [item]);
} else {
collection.push(item);
}
});
return map;
}
/**
* Outputs the object in a better way by hiding all the private methods/attributes.
* @param object - the class to use
*/
export function betterConsoleLog(object: { [key: string]: any }) {
const toPrint: { [key: string]: any } = {};
for (const key in object) {
if (object.hasOwnProperty(key)) {
if (!key.startsWith("_") && key != "originalArgs") {
toPrint[key] = object[key];
}
}
}
return toPrint;
}
/**
* Helper to find if a given object is an array (or similar)
*/
export const isArrayLike = <T>(x: any): x is Array<T> =>
x &&
typeof x.length === "number" &&
typeof x !== "function" &&
typeof x !== "string";
/*
export function addSurrogate(text: string) {
let temp = "";
for (const letter of text) {
const t = letter.charCodeAt(0);
if (0x1000 < t && t < 0x10FFFF) {
const b = Buffer.from(letter, "utf16le");
const r = String.fromCharCode(b.readUInt16LE(0)) + String.fromCharCode(b.readUInt16LE(2));
temp += r;
} else {
text += letter;
}
}
return temp;
}
*/
/**
* Special case signed little ints
* @param big
* @param number
* @returns {Buffer}
*/
export function toSignedLittleBuffer(
big: bigInt.BigInteger | string | number,
number = 8
): Buffer {
const bigNumber = returnBigInt(big);
const byteArray = [];
for (let i = 0; i < number; i++) {
byteArray[i] = bigNumber.shiftRight(8 * i).and(255);
}
// smh hacks
return Buffer.from(byteArray as unknown as number[]);
}
/**
* converts a big int to a buffer
* @param bigIntVar {BigInteger}
* @param bytesNumber
* @param little
* @param signed
* @returns {Buffer}
*/
export function readBufferFromBigInt(
bigIntVar: bigInt.BigInteger,
bytesNumber: number,
little = true,
signed = false
): Buffer {
bigIntVar = bigInt(bigIntVar);
const bitLength = bigIntVar.bitLength().toJSNumber();
const bytes = Math.ceil(bitLength / 8);
if (bytesNumber < bytes) {
throw new Error("OverflowError: int too big to convert");
}
if (!signed && bigIntVar.lesser(bigInt(0))) {
throw new Error("Cannot convert to unsigned");
}
let below = false;
if (bigIntVar.lesser(bigInt(0))) {
below = true;
bigIntVar = bigIntVar.abs();
}
const hex = bigIntVar.toString(16).padStart(bytesNumber * 2, "0");
let littleBuffer = Buffer.from(hex, "hex");
if (little) {
littleBuffer = littleBuffer.reverse();
}
if (signed && below) {
if (little) {
let reminder = false;
if (littleBuffer[0] !== 0) {
littleBuffer[0] -= 1;
}
for (let i = 0; i < littleBuffer.length; i++) {
if (littleBuffer[i] === 0) {
reminder = true;
continue;
}
if (reminder) {
littleBuffer[i] -= 1;
reminder = false;
}
littleBuffer[i] = 255 - littleBuffer[i];
}
} else {
littleBuffer[littleBuffer.length - 1] =
256 - littleBuffer[littleBuffer.length - 1];
for (let i = 0; i < littleBuffer.length - 1; i++) {
littleBuffer[i] = 255 - littleBuffer[i];
}
}
}
return littleBuffer;
}
/**
* Generates a random long integer (8 bytes), which is optionally signed
* @returns {BigInteger}
*/
export function generateRandomLong(signed = true) {
return readBigIntFromBuffer(generateRandomBytes(8), true, signed);
}
/**
* .... really javascript
* @param n {number}
* @param m {number}
* @returns {number}
*/
export function mod(n: number, m: number) {
return ((n % m) + m) % m;
}
/**
* returns a positive bigInt
* @param n {bigInt.BigInteger}
* @param m {bigInt.BigInteger}
* @returns {bigInt.BigInteger}
*/
export function bigIntMod(
n: bigInt.BigInteger,
m: bigInt.BigInteger
): bigInt.BigInteger {
return n.remainder(m).add(m).remainder(m);
}
/**
* Generates a random bytes array
* @param count
* @returns {Buffer}
*/
export function generateRandomBytes(count: number) {
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
}
}
*/
export function stripText(text: string, entities: Api.TypeMessageEntity[]) {
if (!entities || !entities.length) {
return text.trim();
}
while (text && text[text.length - 1].trim() === "") {
const e = entities[entities.length - 1];
if (e.offset + e.length == text.length) {
if (e.length == 1) {
entities.pop();
if (!entities.length) {
return text.trim();
}
} else {
e.length -= 1;
}
}
text = text.slice(0, -1);
}
while (text && text[0].trim() === "") {
for (let i = 0; i < entities.length; i++) {
const e = entities[i];
if (e.offset != 0) {
e.offset--;
continue;
}
if (e.length == 1) {
entities.shift();
if (!entities.length) {
return text.trimLeft();
}
} else {
e.length -= 1;
}
}
text = text.slice(1);
}
return text;
}
/**
* Generates the key data corresponding to the given nonces
* @param serverNonceBigInt
* @param newNonceBigInt
* @returns {{key: Buffer, iv: Buffer}}
*/
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: Buffer) {
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}
*/
export function sha1(data: Buffer): Promise<Buffer> {
const shaSum = crypto.createHash("sha1");
shaSum.update(data);
return shaSum.digest();
}
/**
* Calculates the SHA256 digest for the given data
* @param data
* @returns {Promise}
*/
export function sha256(data: Buffer): Promise<Buffer> {
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}
*/
export function modExp(
a: bigInt.BigInteger,
b: bigInt.BigInteger,
n: bigInt.BigInteger
): 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;
}
/**
* Gets the arbitrary-length byte array corresponding to the given integer
* @param integer {number,BigInteger}
* @param signed {boolean}
* @returns {Buffer}
*/
export function getByteArray(
integer: bigInt.BigInteger | number,
signed = false
) {
const bits = integer.toString(2).length;
const byteLength = Math.floor((bits + 8 - 1) / 8);
return readBufferFromBigInt(
typeof integer == "number" ? bigInt(integer) : integer,
byteLength,
false,
signed
);
}
export function returnBigInt(
num: bigInt.BigInteger | string | number | bigint
) {
if (bigInt.isInstance(num)) {
return num;
}
if (typeof num == "number") {
return bigInt(num);
}
if (typeof num == "bigint") {
return bigInt(num);
}
return bigInt(num);
}
/**
* Helper function to return the smaller big int in an array
* @param arrayOfBigInts
*/
export function getMinBigInt(
arrayOfBigInts: (bigInt.BigInteger | string)[]
): bigInt.BigInteger {
if (arrayOfBigInts.length == 0) {
return bigInt.zero;
}
if (arrayOfBigInts.length == 1) {
return returnBigInt(arrayOfBigInts[0]);
}
let smallest = returnBigInt(arrayOfBigInts[0]);
for (let i = 1; i < arrayOfBigInts.length; i++) {
if (returnBigInt(arrayOfBigInts[i]).lesser(smallest)) {
smallest = returnBigInt(arrayOfBigInts[i]);
}
}
return smallest;
}
/**
* returns a random int from min (inclusive) and max (inclusive)
* @param min
* @param max
* @returns {number}
*/
export function getRandomInt(min: number, max: number) {
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}
*/
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
/**
* Helper to export two buffers of same length
* @returns {Buffer}
*/
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
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: number[] | undefined = undefined;
export function crc32(buf: Buffer | string) {
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;
}
export class TotalList<T> extends Array<T> {
public total?: number;
constructor() {
super();
this.total = 0;
}
}
export const _EntityType = {
USER: 0,
CHAT: 1,
CHANNEL: 2,
};
Object.freeze(_EntityType);
export function _entityType(entity: EntityLike) {
if (typeof entity !== "object" || !("SUBCLASS_OF_ID" in entity)) {
throw new Error(
`${entity} is not a TLObject, cannot determine entity type`
);
}
if (
![
0x2d45687, // crc32('Peer')
0xc91c90b6, // crc32('InputPeer')
0xe669bf46, // crc32('InputUser')
0x40f202fd, // crc32('InputChannel')
0x2da17977, // crc32('User')
0xc5af5d94, // crc32('Chat')
0x1f4661b9, // crc32('UserFull')
0xd49a2697, // crc32('ChatFull')
].includes(entity.SUBCLASS_OF_ID)
) {
throw new Error(`${entity} does not have any entity type`);
}
const name = entity.className;
if (name.includes("User")) {
return _EntityType.USER;
} else if (name.includes("Chat")) {
return _EntityType.CHAT;
} else if (name.includes("Channel")) {
return _EntityType.CHANNEL;
} else if (name.includes("Self")) {
return _EntityType.USER;
}
// 'Empty' in name or not found, we don't care, not a valid entity.
throw new Error(`${entity} does not have any entity type`);
}