348 lines
9.9 KiB
JavaScript
348 lines
9.9 KiB
JavaScript
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(/>|{|}/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<REAL_TYPE>
|
|
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,
|
|
};
|