768 lines
27 KiB
JavaScript
768 lines
27 KiB
JavaScript
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
const ts = require('typescript')
|
|
|
|
const schema = require('../../tl/api-schema.json')
|
|
|
|
const updates = require('./generate-updates.cjs')
|
|
|
|
function findMethodAvailability(method) {
|
|
const entry = schema.e.find(it => it.kind === 'method' && it.name === method)
|
|
if (!entry) return null
|
|
|
|
return entry.available ?? null
|
|
}
|
|
|
|
const targetDir = path.join(__dirname, '../src/highlevel')
|
|
|
|
async function* getFiles(dir) {
|
|
const dirents = await fs.promises.readdir(dir, { withFileTypes: true })
|
|
|
|
for (const dirent of dirents) {
|
|
const res = path.resolve(dir, dirent.name)
|
|
|
|
if (dirent.isDirectory()) {
|
|
yield * getFiles(res)
|
|
} else {
|
|
yield res
|
|
}
|
|
}
|
|
}
|
|
|
|
function throwError(ast, file, text) {
|
|
console.log(
|
|
`An error encountered at ${path.relative(targetDir, file)}:
|
|
> ${ast.getText()}
|
|
${text}`,
|
|
)
|
|
process.exit(0)
|
|
}
|
|
|
|
function visitRecursively(ast, check, callback) {
|
|
const visit = (node) => {
|
|
if (!ts.isNode(node)) return
|
|
|
|
// recursively continue visiting
|
|
for (const [key, value] of Object.entries(node)) {
|
|
if (!value || typeof value !== 'object' || key === 'parent') {
|
|
continue
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach(visit)
|
|
} else {
|
|
visit(value)
|
|
}
|
|
}
|
|
|
|
if (check(node)) {
|
|
callback(node)
|
|
}
|
|
}
|
|
|
|
visit(ast)
|
|
}
|
|
|
|
function findRawApiUsages(ast, fileName) {
|
|
// find `cilent.call({ _: '...', ...})
|
|
|
|
if (ast.kind !== ts.SyntaxKind.FunctionDeclaration) return []
|
|
const firstParamName = ast.parameters[0]?.name?.escapedText
|
|
if (!firstParamName) return []
|
|
|
|
const usages = []
|
|
|
|
visitRecursively(
|
|
ast,
|
|
node => node.kind === ts.SyntaxKind.CallExpression,
|
|
(call) => {
|
|
if (call.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) return
|
|
const prop = call.expression
|
|
|
|
if (
|
|
prop.name.escapedText === 'call'
|
|
&& prop.expression.kind === ts.SyntaxKind.Identifier
|
|
&& prop.expression.escapedText === firstParamName
|
|
) {
|
|
usages.push(call)
|
|
}
|
|
},
|
|
)
|
|
|
|
const methodUsages = []
|
|
|
|
for (const call of usages) {
|
|
const arg = call.arguments[0]
|
|
|
|
if (!arg || arg.kind !== ts.SyntaxKind.ObjectLiteralExpression) {
|
|
throwError(
|
|
call,
|
|
fileName,
|
|
'First argument to this.call() must be an object literal. Please use @available directive manually',
|
|
)
|
|
}
|
|
|
|
const method = arg.properties.find(it => it.name.escapedText === '_')
|
|
|
|
if (!method || method.kind !== ts.SyntaxKind.PropertyAssignment) {
|
|
throwError(call, fileName, 'First argument to this.call() must have a _ property')
|
|
}
|
|
|
|
const init = method.initializer
|
|
|
|
if (init.kind === ts.SyntaxKind.StringLiteral) {
|
|
methodUsages.push(init.text)
|
|
} else if (init.kind === ts.SyntaxKind.ConditionalExpression) {
|
|
const whenTrue = init.whenTrue
|
|
const whenFalse = init.whenFalse
|
|
|
|
if (whenTrue.kind !== ts.SyntaxKind.StringLiteral || whenFalse.kind !== ts.SyntaxKind.StringLiteral) {
|
|
throwError(
|
|
call,
|
|
fileName,
|
|
'Too complex, failed to extract method name, please use @available directive manually',
|
|
)
|
|
}
|
|
|
|
methodUsages.push(whenTrue.text, whenFalse.text)
|
|
} else {
|
|
throwError(
|
|
call,
|
|
fileName,
|
|
'Too complex, failed to extract method name, please use @available directive manually',
|
|
)
|
|
}
|
|
}
|
|
|
|
return methodUsages
|
|
}
|
|
|
|
function findDependencies(ast) {
|
|
const deps = new Set()
|
|
|
|
visitRecursively(
|
|
ast,
|
|
node => node.kind === ts.SyntaxKind.CallExpression,
|
|
(call) => {
|
|
if (call.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) return
|
|
const prop = call.expression
|
|
|
|
if (
|
|
prop.name.escapedText !== 'call'
|
|
&& prop.name.escapedText !== '_emitError'
|
|
&& prop.name.escapedText !== '_cachePeersFrom'
|
|
&& prop.name.escapedText !== 'importSession'
|
|
&& prop.name.escapedText !== 'emit'
|
|
&& prop.expression.kind === ts.SyntaxKind.ThisKeyword
|
|
) {
|
|
deps.add(prop.name.escapedText)
|
|
}
|
|
},
|
|
)
|
|
|
|
return [...deps]
|
|
}
|
|
|
|
function determineCommonAvailability(methods, resolver = v => v) {
|
|
let common = 'both'
|
|
|
|
for (const method of methods) {
|
|
const available = resolver(method)
|
|
|
|
if (available === null) {
|
|
console.log(`availability null for ${method}`)
|
|
|
|
return null
|
|
}
|
|
|
|
if (common === 'both') {
|
|
common = available
|
|
} else if (available !== 'both' && common !== available) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
return common
|
|
}
|
|
|
|
function runEslint(targetFile) {
|
|
require('node:child_process').execSync(`pnpm exec eslint --fix ${targetFile}`, {
|
|
stdio: 'inherit',
|
|
})
|
|
}
|
|
|
|
async function addSingleMethod(state, fileName) {
|
|
const fileFullText = await fs.promises.readFile(fileName, 'utf-8')
|
|
const program = ts.createSourceFile(path.basename(fileName), fileFullText, ts.ScriptTarget.ES2018, true)
|
|
const relPath = path.relative(targetDir, fileName).replace(/\\/g, '/') // replace path delim to unix
|
|
const module = `./${relPath.replace(/\.ts$/, '.js')}`
|
|
|
|
state.files[relPath] = fileFullText
|
|
|
|
function getLeadingComments(ast) {
|
|
return (ts.getLeadingCommentRanges(fileFullText, ast.pos) || [])
|
|
.map(range => fileFullText.substring(range.pos, range.end))
|
|
.join('\n')
|
|
}
|
|
|
|
function checkForFlag(ast, flag) {
|
|
return getLeadingComments(ast)
|
|
.split('\n')
|
|
.map(i => i.replace(/^(\/\/|\s*\*+|\/\*{2,}\s*)/g, '').trim())
|
|
.find(i => i.startsWith(flag))
|
|
}
|
|
|
|
const hasOverloads = {}
|
|
|
|
for (const stmt of program.statements) {
|
|
const isCopy = checkForFlag(stmt, '@copy')
|
|
const isTypeExported = checkForFlag(stmt, '@exported')
|
|
|
|
if (stmt.kind === ts.SyntaxKind.ImportDeclaration) {
|
|
if (!isCopy) continue
|
|
|
|
if (
|
|
!stmt.importClause.namedBindings
|
|
|| stmt.importClause.namedBindings.kind !== ts.SyntaxKind.NamedImports
|
|
) {
|
|
throwError(stmt, fileName, 'Only named imports are supported!')
|
|
}
|
|
|
|
let module = stmt.moduleSpecifier.text
|
|
|
|
if (module[0] === '.') {
|
|
// relative, need to resolve
|
|
const modFullPath = path.join(path.dirname(fileName), stmt.moduleSpecifier.text)
|
|
const modPath = path.dirname(modFullPath)
|
|
const modName = path.basename(modFullPath)
|
|
|
|
module = path.join(path.relative(targetDir, modPath), modName).replace(/\\/g, '/') // replace path delim to unix
|
|
if (module[0] !== '.') module = `./${module}`
|
|
}
|
|
|
|
if (module === './client') {
|
|
throwError(stmt, fileName, "You can't copy an import from ./client")
|
|
}
|
|
|
|
if (!(module in state.imports)) {
|
|
state.imports[module] = new Set()
|
|
}
|
|
|
|
for (const el of stmt.importClause.namedBindings.elements) {
|
|
state.imports[module].add(el.name.escapedText)
|
|
}
|
|
} else if (stmt.kind === ts.SyntaxKind.FunctionDeclaration) {
|
|
const name = stmt.name.escapedText
|
|
|
|
if (checkForFlag(stmt, '@skip')) { continue }
|
|
|
|
if (stmt.body && name in state.methods.used) {
|
|
throwError(
|
|
stmt.name,
|
|
fileName,
|
|
`Function name "${name}" was already used in file ${state.methods.used[name]}`,
|
|
)
|
|
}
|
|
|
|
const isExported = (stmt.modifiers || []).find(mod => mod.kind === ts.SyntaxKind.ExportKeyword)
|
|
const isDeclare = (stmt.modifiers || []).find(mod => mod.kind === ts.SyntaxKind.DeclareKeyword)
|
|
const isInitialize = checkForFlag(stmt, '@initialize')
|
|
const isManualImpl = checkForFlag(stmt, '@manual-impl')
|
|
const isInitializeSuper = isInitialize === 'super'
|
|
const aliases = (function () {
|
|
const flag = checkForFlag(stmt, '@alias')
|
|
if (!flag) return []
|
|
|
|
const [, aliases] = flag.split('=')
|
|
if (!aliases || !aliases.length) return []
|
|
|
|
return aliases.split(',')
|
|
})()
|
|
const available = (function () {
|
|
const flag = checkForFlag(stmt, '@available')
|
|
if (!flag) return null
|
|
|
|
const [, available] = flag.split('=')
|
|
if (!available || !available.length) return null
|
|
|
|
if (available !== 'user' && available !== 'bot' && available !== 'both') {
|
|
throwError(stmt, fileName, `Invalid value for @available flag: ${available}`)
|
|
}
|
|
|
|
return available
|
|
})()
|
|
const rawApiMethods = available === null && findRawApiUsages(stmt, fileName)
|
|
const dependencies = findDependencies(stmt).filter(it => it !== name)
|
|
|
|
if (isInitialize && isExported) {
|
|
throwError(isExported, fileName, 'Initialization methods must not be exported')
|
|
}
|
|
|
|
if (isInitialize) {
|
|
let code = stmt.body.getFullText()
|
|
|
|
// strip leading { and trailing }
|
|
while (code[0] !== '{') code = code.slice(1)
|
|
while (code[code.length - 1] !== '}') code = code.slice(0, -1)
|
|
code = code.slice(1, -1).trim()
|
|
|
|
if (isInitializeSuper) {
|
|
state.init.unshift(code)
|
|
} else {
|
|
state.init.push(code)
|
|
}
|
|
}
|
|
|
|
if (isManualImpl) {
|
|
state.impls.push({
|
|
name: isManualImpl.split('=')[1],
|
|
code: stmt.getFullText(),
|
|
})
|
|
}
|
|
|
|
if (!isExported && !isDeclare) continue
|
|
|
|
const firstArg = stmt.parameters[0]
|
|
|
|
if (isExported && (!firstArg || firstArg.type.getText() !== 'ITelegramClient')) {
|
|
continue
|
|
}
|
|
|
|
// overloads
|
|
const isOverload = !stmt.body
|
|
|
|
if (isOverload) {
|
|
hasOverloads[name] = true
|
|
} else {
|
|
state.methods.used[name] = relPath
|
|
}
|
|
|
|
const isPrivate = checkForFlag(stmt, '@internal')
|
|
const isManual = checkForFlag(stmt, '@manual')
|
|
const isNoemit = checkForFlag(stmt, '@noemit')
|
|
const shouldEmit = !isNoemit && !(isPrivate && !isOverload && !Object.keys(hasOverloads).length)
|
|
|
|
if (shouldEmit) {
|
|
state.methods.list.push({
|
|
from: relPath,
|
|
module,
|
|
name,
|
|
isPrivate,
|
|
isManual,
|
|
isNoemit,
|
|
isDeclare,
|
|
shouldEmit,
|
|
func: stmt,
|
|
comment: getLeadingComments(stmt),
|
|
aliases,
|
|
available,
|
|
rawApiMethods,
|
|
dependencies,
|
|
overload: isOverload,
|
|
hasOverloads: hasOverloads[name] && !isOverload,
|
|
})
|
|
|
|
if (!isDeclare) {
|
|
if (!(module in state.imports)) {
|
|
state.imports[module] = new Set()
|
|
}
|
|
|
|
if (!isManual || isManual.split('=')[1] !== 'noemit') {
|
|
state.imports[module].add(name)
|
|
}
|
|
}
|
|
}
|
|
} else if (stmt.kind === ts.SyntaxKind.InterfaceDeclaration) {
|
|
if (isCopy) {
|
|
state.copy.push({
|
|
from: relPath,
|
|
code: stmt.getText(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
const isExported = (stmt.modifiers || []).find(mod => mod.kind === ts.SyntaxKind.ExportKeyword)
|
|
|
|
if (isTypeExported) {
|
|
if (!isExported) {
|
|
throwError(stmt, fileName, 'Exported interfaces must be exported')
|
|
}
|
|
|
|
if (!(module in state.imports)) {
|
|
state.imports[module] = new Set()
|
|
}
|
|
|
|
state.imports[module].add(stmt.name.escapedText)
|
|
|
|
state.exported[module] = state.exported[module] || new Set()
|
|
state.exported[module].add(stmt.name.escapedText)
|
|
continue
|
|
}
|
|
|
|
if (!checkForFlag(stmt, '@extension')) continue
|
|
|
|
if (isExported) {
|
|
throwError(isExported, fileName, 'Extension interfaces must not be imported')
|
|
}
|
|
if (stmt.heritageClauses && stmt.heritageClauses.length) {
|
|
throwError(stmt.heritageClauses[0], fileName, 'Extension interfaces must not be extended')
|
|
}
|
|
|
|
for (const member of stmt.members || []) {
|
|
state.fields.push({
|
|
from: relPath,
|
|
code: member.getText(),
|
|
})
|
|
}
|
|
} else if (stmt.kind === ts.SyntaxKind.TypeAliasDeclaration && isTypeExported) {
|
|
const isExported = (stmt.modifiers || []).find(mod => mod.kind === ts.SyntaxKind.ExportKeyword)
|
|
|
|
if (!isExported) {
|
|
throwError(stmt, fileName, 'Exported type aliases must be exported')
|
|
}
|
|
|
|
if (!(module in state.imports)) {
|
|
state.imports[module] = new Set()
|
|
}
|
|
|
|
state.imports[module].add(stmt.name.escapedText)
|
|
|
|
state.exported[module] = state.exported[module] || new Set()
|
|
state.exported[module].add(stmt.name.escapedText)
|
|
} else if (isCopy) {
|
|
state.copy.push({ from: relPath, code: stmt.getFullText().trim() })
|
|
} else if (isTypeExported) {
|
|
throwError(stmt, fileName, 'Only functions and interfaces can be exported')
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const targetFile = path.join(__dirname, '../src/highlevel/client.ts')
|
|
const output = fs.createWriteStream(targetFile)
|
|
const state = {
|
|
imports: {},
|
|
exported: {},
|
|
fields: [],
|
|
init: [],
|
|
methods: {
|
|
used: {},
|
|
list: [],
|
|
},
|
|
impls: [],
|
|
copy: [],
|
|
files: {},
|
|
}
|
|
|
|
for await (const file of getFiles(path.join(__dirname, '../src/highlevel/methods'))) {
|
|
if (!file.startsWith('.') && file.endsWith('.ts') && !file.endsWith('.web.ts') && !file.endsWith('.test.ts')) {
|
|
await addSingleMethod(state, file)
|
|
}
|
|
}
|
|
|
|
output.write(
|
|
'/* eslint-disable ts/no-unsafe-declaration-merging, ts/no-unsafe-argument */\n'
|
|
+ '/* THIS FILE WAS AUTO-GENERATED */\n'
|
|
+ "import { Emitter } from '@fuman/utils'\n"
|
|
+ "import Long from 'long'\n",
|
|
)
|
|
Object.entries(state.imports).forEach(([module, items]) => {
|
|
items = [...items]
|
|
if (!items.length) return
|
|
output.write(`import { ${items.sort().join(', ')} } from '${module}'\n`)
|
|
})
|
|
output.write("import { withParams } from './methods/misc/with-params.js'")
|
|
|
|
output.write('\n')
|
|
|
|
state.copy.forEach(({ from, code }) => {
|
|
output.write(`// from ${from}\n${code}\n`)
|
|
})
|
|
|
|
output.write('\nexport interface TelegramClient extends ITelegramClient {\n')
|
|
|
|
output.write(`/** Raw update emitter */
|
|
readonly onRawUpdate: Emitter<RawUpdateInfo>
|
|
/** Parsed update emitter */
|
|
readonly onUpdate: Emitter<ParsedUpdate>`)
|
|
|
|
updates.types.forEach((type) => {
|
|
output.write(`/** ${updates.toSentence(type, 'inline')} */\n`)
|
|
output.write(`readonly on${type.handlerTypeName}: Emitter<${type.updateType}>\n`)
|
|
})
|
|
|
|
output.write(`/**
|
|
* Wrap this client so that all RPC calls will use the specified parameters.
|
|
*
|
|
* @param params Parameters to use
|
|
* @returns Wrapped client
|
|
*/
|
|
withParams(params: RpcCallOptions): this\n`)
|
|
|
|
const printer = ts.createPrinter()
|
|
|
|
const classContents = []
|
|
const classProtoDecls = []
|
|
|
|
state.methods.list.forEach(
|
|
({
|
|
name: origName,
|
|
// isPrivate,
|
|
isManual,
|
|
func,
|
|
comment,
|
|
aliases,
|
|
overload,
|
|
hasOverloads,
|
|
available,
|
|
rawApiMethods,
|
|
dependencies,
|
|
isDeclare,
|
|
}) => {
|
|
if (!available && !overload) {
|
|
// no @available directive
|
|
// try to determine it automatically
|
|
const checkDepsAvailability = (deps) => {
|
|
return determineCommonAvailability(deps, (name) => {
|
|
const method = state.methods.list.find(it => it.name === name && !it.overload)
|
|
|
|
if (!method) {
|
|
throwError(
|
|
func,
|
|
origName,
|
|
`Cannot determine availability of ${name}, is it a client method? Please use @available directive manually`,
|
|
)
|
|
}
|
|
|
|
if (method.available === null) {
|
|
return determineCommonAvailability([
|
|
determineCommonAvailability(method.rawApiMethods, findMethodAvailability),
|
|
checkDepsAvailability(method.dependencies),
|
|
])
|
|
}
|
|
|
|
return method.available
|
|
})
|
|
}
|
|
|
|
available = determineCommonAvailability([
|
|
determineCommonAvailability(rawApiMethods, findMethodAvailability),
|
|
checkDepsAvailability(dependencies),
|
|
])
|
|
}
|
|
|
|
// create method that calls that function and passes `this`
|
|
// first let's determine the signature
|
|
const returnType = func.type ? `: ${func.type.getText()}` : ''
|
|
const generics = func.typeParameters
|
|
? `<${func.typeParameters.map(it => it.getFullText()).join(', ')}>`
|
|
: ''
|
|
const rawParams = (func.parameters || []).filter(
|
|
it => !it.type || it.type.getText() !== 'ITelegramClient',
|
|
)
|
|
const parameters = rawParams
|
|
.map((it) => {
|
|
if (it.initializer) {
|
|
// has default value
|
|
it._savedDefault = it.initializer.getFullText()
|
|
|
|
if (!it.type) {
|
|
// no explicit type.
|
|
// infer from initializer
|
|
if (
|
|
it.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
|| it.initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
) {
|
|
it.type = { kind: ts.SyntaxKind.BooleanKeyword }
|
|
} else if (it.initializer.kind === ts.SyntaxKind.StringLiteral) {
|
|
it.type = { kind: ts.SyntaxKind.StringKeyword }
|
|
} else if (
|
|
it.initializer.kind === ts.SyntaxKind.NumericLiteral
|
|
|| (
|
|
it.initializer.kind === ts.SyntaxKind.Identifier
|
|
&& (it.initializer.escapedText === 'NaN' || it.initializer.escapedText === 'Infinity')
|
|
)
|
|
) {
|
|
it.type = { kind: ts.SyntaxKind.NumberKeyword }
|
|
} else {
|
|
throwError(it, state.methods.used[origName], 'Cannot infer parameter type')
|
|
}
|
|
}
|
|
it.initializer = undefined
|
|
|
|
const deleteParents = (obj) => {
|
|
if (Array.isArray(obj)) {
|
|
return obj.forEach(it => deleteParents(it))
|
|
}
|
|
|
|
if (obj.parent) delete obj.parent
|
|
|
|
for (const k of Object.keys(obj)) {
|
|
if (obj[k] && typeof obj[k] === 'object') {
|
|
deleteParents(obj[k])
|
|
}
|
|
}
|
|
}
|
|
deleteParents(it)
|
|
|
|
it.questionToken = { kind: ts.SyntaxKind.QuestionToken }
|
|
|
|
return printer.printNode(
|
|
ts.EmitHint.Unspecified,
|
|
it,
|
|
// state.files[state.methods.used[origName]]
|
|
)
|
|
}
|
|
|
|
return it.getFullText()
|
|
})
|
|
.join(', ')
|
|
|
|
// remove @internal mark and set default values for parameters
|
|
comment = comment
|
|
.replace(/^\s*\/{2,}\s*@(alias|available|manual).*$/gm, '')
|
|
.replace(/(\n|\/\*)\s*\*\s*@internal.*/, '')
|
|
.replace(/((?:\n|\/\*)\s*\*\s*@param )(\S+)($|\s+)/gm, (_, pref, arg, post) => {
|
|
const param = rawParams.find(it => it.name.escapedText === arg)
|
|
if (!param) return _
|
|
if (!param._savedDefault) return _
|
|
|
|
return `${pref}[${arg}=${param._savedDefault.trim()}]${post}`
|
|
})
|
|
// eslint-disable-next-line regexp/optimal-lookaround-quantifier
|
|
.replace(/(?<=\/\*.*)(?=\n\s*\*\s*(?:@[a-z]+|\/))/s, () => {
|
|
switch (available) {
|
|
case 'user':
|
|
return '\n * **Available**: 👤 users only\n *'
|
|
case 'bot':
|
|
return '\n * **Available**: 🤖 bots only\n *'
|
|
case 'both':
|
|
return '\n * **Available**: ✅ both users and bots\n *'
|
|
}
|
|
|
|
return ''
|
|
})
|
|
|
|
for (const name of [origName, ...aliases]) {
|
|
if (!hasOverloads) {
|
|
if (!comment.match(/\/\*\*?\s*\*\//)) {
|
|
// empty comment, no need to write it
|
|
output.write(`${comment}\n`)
|
|
}
|
|
|
|
output.write(`${name}${generics}(${parameters})${returnType}\n`)
|
|
}
|
|
|
|
if (!overload && !isManual && !isDeclare) {
|
|
if (hasOverloads) {
|
|
classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc')
|
|
}
|
|
classProtoDecls.push(`TelegramClient.prototype.${name} = function(...args) {`)
|
|
|
|
if (hasOverloads) {
|
|
classProtoDecls.push('// @ts-expect-error this kinda breaks typings for overloads, idc')
|
|
}
|
|
classProtoDecls.push(` return ${origName}(this._client, ...args);`)
|
|
classProtoDecls.push('}')
|
|
}
|
|
}
|
|
},
|
|
)
|
|
output.write('}\n')
|
|
|
|
output.write('\nexport type { TelegramClientOptions }\n')
|
|
output.write('\nexport * from "./base.js"\n')
|
|
output.write('\nexport class TelegramClient implements ITelegramClient {\n')
|
|
|
|
output.write(' _client: ITelegramClient\n')
|
|
state.fields.forEach(({ code }) => output.write(`protected ${code}\n`))
|
|
|
|
output.write('constructor(opts: TelegramClientOptions) {\n')
|
|
output.write(' ;(this as any).onRawUpdate = new Emitter()\n')
|
|
output.write(' ;(this as any).onUpdate = new Emitter()\n')
|
|
updates.types.forEach((type) => {
|
|
// we use declaration merging so we can't simply write into this because it thinks it's readonly and already has a value
|
|
output.write(` ;(this as any).on${type.handlerTypeName} = new Emitter()\n`)
|
|
})
|
|
state.init.forEach((code) => {
|
|
code = code.replace('// @generate-update-emitter', () => {
|
|
const lines = [
|
|
' switch (update.name) {',
|
|
]
|
|
updates.types.forEach((type) => {
|
|
lines.push(` case '${type.typeName}':`)
|
|
lines.push(` this.on${type.handlerTypeName}.emit(update.data)`)
|
|
lines.push(' break')
|
|
})
|
|
lines.push(' }')
|
|
return lines.join('\n')
|
|
})
|
|
output.write(`${code}\n`)
|
|
})
|
|
output.write('}\n')
|
|
|
|
classContents.forEach(line => output.write(`${line}\n`))
|
|
|
|
output.write(` withParams(params: RpcCallOptions): this {
|
|
return withParams(this, params)
|
|
}\n`)
|
|
|
|
output.write('}\n')
|
|
|
|
classProtoDecls.forEach(line => output.write(`${line}\n`))
|
|
// proxied methods
|
|
;[
|
|
'prepare',
|
|
'connect',
|
|
'close',
|
|
'notifyLoggedIn',
|
|
'notifyLoggedOut',
|
|
'notifyChannelOpened',
|
|
'notifyChannelClosed',
|
|
'startUpdatesLoop',
|
|
'stopUpdatesLoop',
|
|
'call',
|
|
'importSession',
|
|
'exportSession',
|
|
'handleClientUpdate',
|
|
'getApiCredentials',
|
|
'getPoolSize',
|
|
'getPrimaryDcId',
|
|
'computeSrpParams',
|
|
'computeNewPasswordHash',
|
|
'changePrimaryDc',
|
|
'getMtprotoMessageId',
|
|
'recreateDc',
|
|
].forEach((name) => {
|
|
output.write(
|
|
`TelegramClient.prototype.${name} = function(...args) {\n`
|
|
+ ` return this._client.${name}(...args)\n`
|
|
+ '}\n',
|
|
)
|
|
})
|
|
state.impls.forEach(({ name, code }) => output.write(`TelegramClient.prototype.${name} = ${code}\n`))
|
|
|
|
// write methods re-exports to separate file
|
|
const targetFileMethods = path.join(__dirname, '../src/highlevel/methods.ts')
|
|
const outputMethods = fs.createWriteStream(targetFileMethods)
|
|
|
|
outputMethods.write('/* THIS FILE WAS AUTO-GENERATED */\n')
|
|
state.methods.list.forEach(({ module, name, overload, isDeclare }) => {
|
|
if (overload || isDeclare) return
|
|
outputMethods.write(`export { ${name} } from '${module}'\n`)
|
|
|
|
if (state.exported[module]) {
|
|
outputMethods.write(`export type { ${[...state.exported[module]].join(', ')} } from '${module}'\n`)
|
|
delete state.exported[module]
|
|
}
|
|
})
|
|
|
|
await new Promise((resolve) => { outputMethods.end(resolve) })
|
|
await new Promise((resolve) => { output.end(resolve) })
|
|
|
|
runEslint(targetFile)
|
|
runEslint(targetFileMethods)
|
|
}
|
|
|
|
main().catch(console.error)
|