const snakeToCamelCase = (name) => { const result = name.replace(/(?:^|_)([a-z])/g, (_, g) => g.toUpperCase()); return result.replace(/_/g, ''); }; const variableSnakeToCamelCase = (str) => 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 ]); // This is copy-pasted from `gramjs/Helpers.js` to not depend on TypeScript modules 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; } const findAll = (regex, str, matches = []) => { 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; }; const fromLine = (line, isFunction) => { 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 = { name: match[1], constructorId: parseInt(match[2], 16), argsConfig: {}, subclassOfId: crc32(match[3]), result: match[3], 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(/ \w+:flags\.\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) { 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, argType) { name = name === 'self' ? 'is_self' : name; // Default values const currentConfig = { isVector: false, isFlag: false, skipConstructorId: false, flagIndex: -1, flagIndicator: true, type: undefined, useVectorId: undefined, }; // 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 called 'flags' const flagMatch = currentConfig.type.match(/flags.(\d+)\?([\w<>.]+)/); if (flagMatch) { currentConfig.isFlag = true; currentConfig.flagIndex = Number(flagMatch[1]); // Update the type to match the exact type, not the "flagged" one [, , currentConfig.type] = flagMatch; } // Then check if the type is a Vector 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. 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 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'; // } } return currentConfig; } function* parseTl(content, layer, methods = [], ignoreIds = CORE_TYPES) { (methods || []).reduce((o, m) => ({ ...o, [m.name]: m, }), {}); const objAll = []; const objByName = {}; const objByType = {}; 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) { 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) { // console.log(obj) 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; } } function serializeBytes(data) { 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); } function serializeDate(dt) { 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`); } module.exports = { findAll, parseTl, buildArgConfig, fromLine, CORE_TYPES, serializeDate, serializeBytes, snakeToCamelCase, variableSnakeToCamelCase, };