docs: generate user/bot availability automagically
closes MTQ-85
This commit is contained in:
parent
ec55cb37f7
commit
ff75d40e78
7 changed files with 695 additions and 32 deletions
|
@ -4,6 +4,15 @@ const fs = require('fs')
|
||||||
const prettier = require('prettier')
|
const prettier = require('prettier')
|
||||||
const updates = require('./generate-updates')
|
const updates = require('./generate-updates')
|
||||||
|
|
||||||
|
const schema = require('../../tl/api-schema.json')
|
||||||
|
|
||||||
|
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')
|
const targetDir = path.join(__dirname, '../src')
|
||||||
|
|
||||||
async function* getFiles(dir) {
|
async function* getFiles(dir) {
|
||||||
|
@ -29,6 +38,145 @@ ${text}`,
|
||||||
process.exit(0)
|
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 `this.call({ _: '...', ...})
|
||||||
|
|
||||||
|
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.ThisKeyword) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
async function addSingleMethod(state, fileName) {
|
async function addSingleMethod(state, fileName) {
|
||||||
const fileFullText = await fs.promises.readFile(fileName, 'utf-8')
|
const fileFullText = await fs.promises.readFile(fileName, 'utf-8')
|
||||||
const program = ts.createSourceFile(path.basename(fileName), fileFullText, ts.ScriptTarget.ES2018, true)
|
const program = ts.createSourceFile(path.basename(fileName), fileFullText, ts.ScriptTarget.ES2018, true)
|
||||||
|
@ -117,6 +265,21 @@ async function addSingleMethod(state, fileName) {
|
||||||
|
|
||||||
return aliases.split(',')
|
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 (!isExported && !isPrivate) {
|
if (!isExported && !isPrivate) {
|
||||||
throwError(stmt, fileName, 'Public methods MUST be exported.')
|
throwError(stmt, fileName, 'Public methods MUST be exported.')
|
||||||
|
@ -178,6 +341,9 @@ async function addSingleMethod(state, fileName) {
|
||||||
func: stmt,
|
func: stmt,
|
||||||
comment: getLeadingComments(stmt),
|
comment: getLeadingComments(stmt),
|
||||||
aliases,
|
aliases,
|
||||||
|
available,
|
||||||
|
rawApiMethods,
|
||||||
|
dependencies,
|
||||||
overload: isOverload,
|
overload: isOverload,
|
||||||
hasOverloads: hasOverloads[name] && !isOverload,
|
hasOverloads: hasOverloads[name] && !isOverload,
|
||||||
})
|
})
|
||||||
|
@ -270,7 +436,7 @@ async function main() {
|
||||||
output.write(
|
output.write(
|
||||||
'/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, @typescript-eslint/unified-signatures */\n' +
|
'/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, @typescript-eslint/unified-signatures */\n' +
|
||||||
'/* THIS FILE WAS AUTO-GENERATED */\n' +
|
'/* THIS FILE WAS AUTO-GENERATED */\n' +
|
||||||
"import { BaseTelegramClient, BaseTelegramClientOptions, tl } from '@mtcute/core'\n",
|
"import { BaseTelegramClient, BaseTelegramClientOptions, tl, Long } from '@mtcute/core'\n",
|
||||||
)
|
)
|
||||||
Object.entries(state.imports).forEach(([module, items]) => {
|
Object.entries(state.imports).forEach(([module, items]) => {
|
||||||
items = [...items]
|
items = [...items]
|
||||||
|
@ -323,7 +489,42 @@ on(name: '${type.typeName}', handler: ((upd: ${type.updateType}) => void)): this
|
||||||
aliases,
|
aliases,
|
||||||
overload,
|
overload,
|
||||||
hasOverloads,
|
hasOverloads,
|
||||||
|
available,
|
||||||
|
rawApiMethods,
|
||||||
|
dependencies,
|
||||||
}) => {
|
}) => {
|
||||||
|
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`
|
// create method that calls that function and passes `this`
|
||||||
// first let's determine the signature
|
// first let's determine the signature
|
||||||
const returnType = func.type ? ': ' + func.type.getText() : ''
|
const returnType = func.type ? ': ' + func.type.getText() : ''
|
||||||
|
@ -389,18 +590,27 @@ on(name: '${type.typeName}', handler: ((upd: ${type.updateType}) => void)): this
|
||||||
|
|
||||||
// remove @internal mark and set default values for parameters
|
// remove @internal mark and set default values for parameters
|
||||||
comment = comment
|
comment = comment
|
||||||
.replace(/^\s*\/\/+\s*@alias.*$/m, '')
|
.replace(/^\s*\/\/+\s*@(alias|available).*$/m, '')
|
||||||
.replace(/(\n^|\/\*)\s*\*\s*@internal.*/m, '')
|
.replace(/(\n^|\/\*)\s*\*\s*@internal.*/m, '')
|
||||||
.replace(/((?:\n^|\/\*)\s*\*\s*@param )([^\s]+?)($|\s+)/gm, (_, pref, arg, post) => {
|
.replace(/((?:\n^|\/\*)\s*\*\s*@param )([^\s]+?)($|\s+)/gm, (_, pref, arg, post) => {
|
||||||
const param = rawParams.find((it) => it.name.escapedText === arg)
|
const param = rawParams.find((it) => it.name.escapedText === arg)
|
||||||
if (!param) return _
|
if (!param) return _
|
||||||
if (!param._savedDefault) return _
|
if (!param._savedDefault) return _
|
||||||
|
|
||||||
if (post) {
|
return `${pref}[${arg}=${param._savedDefault.trim()}]${post}`
|
||||||
return `${pref}${arg}${post}(default: \`${param._savedDefault.trim()}\`) `
|
})
|
||||||
|
// insert "some text" at the end of comment before jsdoc
|
||||||
|
.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 `${pref}${arg}\n* (default: \`${param._savedDefault.trim()}\`)`
|
return ''
|
||||||
})
|
})
|
||||||
|
|
||||||
for (const name of [origName, ...aliases]) {
|
for (const name of [origName, ...aliases]) {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,7 @@ import { TelegramClient } from '../../client'
|
||||||
import { MaybeDynamic, SentCode, TermsOfService, User } from '../../types'
|
import { MaybeDynamic, SentCode, TermsOfService, User } from '../../types'
|
||||||
import { normalizePhoneNumber, resolveMaybeDynamic } from '../../utils/misc-utils'
|
import { normalizePhoneNumber, resolveMaybeDynamic } from '../../utils/misc-utils'
|
||||||
|
|
||||||
|
// @available=both
|
||||||
/**
|
/**
|
||||||
* Start the client in an interactive and declarative manner,
|
* Start the client in an interactive and declarative manner,
|
||||||
* by providing callbacks for authorization details.
|
* by providing callbacks for authorization details.
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { tl } from '@mtcute/core'
|
import { Long } from '@mtcute/core'
|
||||||
|
|
||||||
import { TelegramClient } from '../../client'
|
import { TelegramClient } from '../../client'
|
||||||
|
|
||||||
|
// @available=bot
|
||||||
/**
|
/**
|
||||||
* Send an answer to a callback query.
|
* Send an answer to a callback query.
|
||||||
*
|
*
|
||||||
|
@ -11,7 +12,7 @@ import { TelegramClient } from '../../client'
|
||||||
*/
|
*/
|
||||||
export async function answerCallbackQuery(
|
export async function answerCallbackQuery(
|
||||||
this: TelegramClient,
|
this: TelegramClient,
|
||||||
queryId: tl.Long,
|
queryId: Long,
|
||||||
params?: {
|
params?: {
|
||||||
/**
|
/**
|
||||||
* Maximum amount of time in seconds for which
|
* Maximum amount of time in seconds for which
|
||||||
|
|
|
@ -31,6 +31,7 @@ const REQUESTS_PER_CONNECTION = 3
|
||||||
const MAX_PART_COUNT = 4000 // 512 kb * 4000 = 2000 MiB
|
const MAX_PART_COUNT = 4000 // 512 kb * 4000 = 2000 MiB
|
||||||
const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB
|
const MAX_PART_COUNT_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB
|
||||||
|
|
||||||
|
// @available=both
|
||||||
/**
|
/**
|
||||||
* Upload a file to Telegram servers, without actually
|
* Upload a file to Telegram servers, without actually
|
||||||
* sending a message anywhere. Useful when an `InputFile` is required.
|
* sending a message anywhere. Useful when an `InputFile` is required.
|
||||||
|
|
|
@ -41,6 +41,7 @@ export async function getMessages(
|
||||||
fromReply?: boolean,
|
fromReply?: boolean,
|
||||||
): Promise<(Message | null)[]>
|
): Promise<(Message | null)[]>
|
||||||
|
|
||||||
|
// @available=both
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export async function getMessages(
|
export async function getMessages(
|
||||||
this: TelegramClient,
|
this: TelegramClient,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { TelegramClient } from '../../client'
|
||||||
import { InputPeerLike, MtPeerNotFoundError } from '../../types'
|
import { InputPeerLike, MtPeerNotFoundError } from '../../types'
|
||||||
import { normalizeToInputPeer } from '../../utils/peer-utils'
|
import { normalizeToInputPeer } from '../../utils/peer-utils'
|
||||||
|
|
||||||
|
// @available=both
|
||||||
/**
|
/**
|
||||||
* Get the `InputPeer` of a known peer id.
|
* Get the `InputPeer` of a known peer id.
|
||||||
* Useful when an `InputPeer` is needed.
|
* Useful when an `InputPeer` is needed.
|
||||||
|
|
Loading…
Reference in a new issue