UNPKG

dograma

Version:

NodeJS/Browser MTProto API Telegram client library,

347 lines (303 loc) 10.4 kB
import { crc32 } from "../Helpers"; import type { DateLike } from "../define"; const snakeToCamelCase = (name: string) => { const result = name.replace(/(?:^|_)([a-z])/g, (_, g) => g.toUpperCase()); return result.replace(/_/g, ""); }; const variableSnakeToCamelCase = (str: string) => str.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("-", "").replace("_", "") ); const CORE_TYPES = new Set([ 0xbc799737, // boolFalse#bc799737 = Bool; 0x997275b5, // boolTrue#997275b5 = Bool; 0x3fedd339, // true#3fedd339 = True; 0xc4b9f9bb, // error#c4b9f9bb code:int text:string = Error; 0x56730bcc, // null#56730bcc = Null; ]); const AUTH_KEY_TYPES = new Set([ 0x05162463, // resPQ, 0x83c95aec, // p_q_inner_data 0xa9f55f95, // p_q_inner_data_dc 0x3c6a84d4, // p_q_inner_data_temp 0x56fddf88, // p_q_inner_data_temp_dc 0xd0e8075c, // server_DH_params_ok 0xb5890dba, // server_DH_inner_data 0x6643b654, // client_DH_inner_data 0xd712e4be, // req_DH_params 0xf5045f1f, // set_client_DH_params 0x3072cfa1, // gzip_packed ]); const fromLine = (line: string, isFunction: boolean) => { const match = line.match( /([\w.]+)(?:#([0-9a-fA-F]+))?(?:\s{?\w+:[\w\d<>#.?!]+}?)*\s=\s([\w\d<>#.?]+);$/ ); if (!match) { // Probably "vector#1cb5c415 {t:Type} # [ t ] = Vector t;" throw new Error(`Cannot parse TLObject ${line}`); } const argsMatch = findAll(/({)?(\w+):([\w\d<>#.?!]+)}?/, line); const currentConfig: any = { name: match[1], constructorId: parseInt(match[2], 16), argsConfig: {}, subclassOfId: crc32(match[3]), result: match[3], isFunction: isFunction, namespace: undefined, }; if (!currentConfig.constructorId) { const hexId = ""; let args; if (Object.values(currentConfig.argsConfig).length) { args = ` ${Object.keys(currentConfig.argsConfig) .map((arg) => arg.toString()) .join(" ")}`; } else { args = ""; } const representation = `${currentConfig.name}${hexId}${args} = ${currentConfig.result}` .replace(/(:|\?)bytes /g, "$1string ") .replace(/</g, " ") .replace(/>|{|}/g, "") .replace(/ \w+:flags(\d+)?\.\d+\?true/g, ""); if (currentConfig.name === "inputMediaInvoice") { // eslint-disable-next-line no-empty if (currentConfig.name === "inputMediaInvoice") { } } currentConfig.constructorId = crc32( Buffer.from(representation, "utf8") ); } for (const [brace, name, argType] of argsMatch) { if (brace === undefined) { // @ts-ignore currentConfig.argsConfig[variableSnakeToCamelCase(name)] = buildArgConfig(name, argType); } } if (currentConfig.name.includes(".")) { [currentConfig.namespace, currentConfig.name] = currentConfig.name.split(/\.(.+)/); } currentConfig.name = snakeToCamelCase(currentConfig.name); /* for (const arg in currentConfig.argsConfig){ if (currentConfig.argsConfig.hasOwnProperty(arg)){ if (currentConfig.argsConfig[arg].flagIndicator){ delete currentConfig.argsConfig[arg] } } }*/ return currentConfig; }; function buildArgConfig(name: string, argType: string) { name = name === "self" ? "is_self" : name; // Default values const currentConfig: any = { isVector: false, isFlag: false, skipConstructorId: false, flagName: null, flagIndex: -1, flagIndicator: true, type: null, useVectorId: null, }; // Special case: some types can be inferred, which makes it // less annoying to type. Currently the only type that can // be inferred is if the name is 'random_id', to which a // random ID will be assigned if left as None (the default) const canBeInferred = name === "random_id"; // The type can be an indicator that other arguments will be flags if (argType !== "#") { currentConfig.flagIndicator = false; // Strip the exclamation mark always to have only the name currentConfig.type = argType.replace(/^!+/, ""); // The type may be a flag (flags.IDX?REAL_TYPE) // Note that 'flags' is NOT the flags name; this // is determined by a previous argument // However, we assume that the argument will always be starts with 'flags' // @ts-ignore const flagMatch = currentConfig.type.match( /(flags(?:\d+)?).(\d+)\?([\w<>.]+)/ ); if (flagMatch) { currentConfig.isFlag = true; // As of layer 140, flagName can be "flags" or "flags2" currentConfig.flagName = flagMatch[1]; currentConfig.flagIndex = Number(flagMatch[2]); // Update the type to match the exact type, not the "flagged" one currentConfig.type = flagMatch[3]; } // Then check if the type is a Vector<REAL_TYPE> // @ts-ignore const vectorMatch = currentConfig.type.match(/[Vv]ector<([\w\d.]+)>/); if (vectorMatch) { currentConfig.isVector = true; // If the type's first letter is not uppercase, then // it is a constructor and we use (read/write) its ID. // @ts-ignore currentConfig.useVectorId = currentConfig.type.charAt(0) === "V"; // Update the type to match the one inside the vector [, currentConfig.type] = vectorMatch; } // See use_vector_id. An example of such case is ipPort in // help.configSpecial // @ts-ignore if (/^[a-z]$/.test(currentConfig.type.split(".").pop().charAt(0))) { currentConfig.skipConstructorId = true; } // The name may contain "date" in it, if this is the case and // the type is "int", we can safely assume that this should be // treated as a "date" object. Note that this is not a valid // Telegram object, but it's easier to work with // if ( // this.type === 'int' && // (/(\b|_)([dr]ate|until|since)(\b|_)/.test(name) || // ['expires', 'expires_at', 'was_online'].includes(name)) // ) { // this.type = 'date'; // } } // workaround if (currentConfig.type == "future_salt") { currentConfig.type = "FutureSalt"; } return currentConfig; } const parseTl = function* ( content: string, layer: string, methods: any[] = [], ignoreIds = CORE_TYPES ) { const methodInfo = (methods || []).reduce( (o, m) => ({ ...o, [m.name]: m }), {} ); const objAll = []; const objByName: any = {}; const objByType: any = {}; const file = content; let isFunction = false; for (let line of file.split("\n")) { const commentIndex = line.indexOf("//"); if (commentIndex !== -1) { line = line.slice(0, commentIndex); } line = line.trim(); if (!line) { continue; } const match = line.match(/---(\w+)---/); if (match) { const [, followingTypes] = match; isFunction = followingTypes === "functions"; continue; } try { const result = fromLine(line, isFunction); if (ignoreIds.has(result.constructorId)) { continue; } objAll.push(result); if (!result.isFunction) { if (!objByType[result.result]) { objByType[result.result] = []; } objByName[result.name] = result; objByType[result.result].push(result); } } catch (e: any) { if (!e.toString().includes("vector#1cb5c415")) { throw e; } } } // Once all objects have been parsed, replace the // string type from the arguments with references for (const obj of objAll) { if (AUTH_KEY_TYPES.has(obj.constructorId)) { for (const arg in obj.argsConfig) { if (obj.argsConfig[arg].type === "string") { obj.argsConfig[arg].type = "bytes"; } } } } for (const obj of objAll) { yield obj; } }; const findAll = (regex: RegExp, str: string, matches: any = []) => { if (!regex.flags.includes("g")) { regex = new RegExp(regex.source, "g"); } const res = regex.exec(str); if (res) { matches.push(res.slice(1)); findAll(regex, str, matches); } return matches; }; export function serializeBytes(data: Buffer | string | any) { if (!(data instanceof Buffer)) { if (typeof data == "string") { data = Buffer.from(data); } else { throw Error(`Bytes or str expected, not ${data.constructor.name}`); } } const r = []; let padding; if (data.length < 254) { padding = (data.length + 1) % 4; if (padding !== 0) { padding = 4 - padding; } r.push(Buffer.from([data.length])); r.push(data); } else { padding = data.length % 4; if (padding !== 0) { padding = 4 - padding; } r.push( Buffer.from([ 254, data.length % 256, (data.length >> 8) % 256, (data.length >> 16) % 256, ]) ); r.push(data); } r.push(Buffer.alloc(padding).fill(0)); return Buffer.concat(r); } export function serializeDate(dt: DateLike | Date) { if (!dt) { return Buffer.alloc(4).fill(0); } if (dt instanceof Date) { dt = Math.floor((Date.now() - dt.getTime()) / 1000); } if (typeof dt == "number") { const t = Buffer.alloc(4); t.writeInt32LE(dt, 0); return t; } throw Error(`Cannot interpret "${dt}" as a date`); } export { findAll, parseTl, buildArgConfig, fromLine, CORE_TYPES, snakeToCamelCase, variableSnakeToCamelCase, };