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 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')
|
||||
|
||||
async function* getFiles(dir) {
|
||||
|
@ -29,6 +38,145 @@ ${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 `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) {
|
||||
const fileFullText = await fs.promises.readFile(fileName, 'utf-8')
|
||||
const program = ts.createSourceFile(path.basename(fileName), fileFullText, ts.ScriptTarget.ES2018, true)
|
||||
|
@ -117,6 +265,21 @@ async function addSingleMethod(state, fileName) {
|
|||
|
||||
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) {
|
||||
throwError(stmt, fileName, 'Public methods MUST be exported.')
|
||||
|
@ -178,6 +341,9 @@ async function addSingleMethod(state, fileName) {
|
|||
func: stmt,
|
||||
comment: getLeadingComments(stmt),
|
||||
aliases,
|
||||
available,
|
||||
rawApiMethods,
|
||||
dependencies,
|
||||
overload: isOverload,
|
||||
hasOverloads: hasOverloads[name] && !isOverload,
|
||||
})
|
||||
|
@ -270,7 +436,7 @@ async function main() {
|
|||
output.write(
|
||||
'/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, @typescript-eslint/unified-signatures */\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]) => {
|
||||
items = [...items]
|
||||
|
@ -323,7 +489,42 @@ on(name: '${type.typeName}', handler: ((upd: ${type.updateType}) => void)): this
|
|||
aliases,
|
||||
overload,
|
||||
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`
|
||||
// first let's determine the signature
|
||||
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
|
||||
comment = comment
|
||||
.replace(/^\s*\/\/+\s*@alias.*$/m, '')
|
||||
.replace(/^\s*\/\/+\s*@(alias|available).*$/m, '')
|
||||
.replace(/(\n^|\/\*)\s*\*\s*@internal.*/m, '')
|
||||
.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 _
|
||||
|
||||
if (post) {
|
||||
return `${pref}${arg}${post}(default: \`${param._savedDefault.trim()}\`) `
|
||||
return `${pref}[${arg}=${param._savedDefault.trim()}]${post}`
|
||||
})
|
||||
// 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]) {
|
||||
|
|
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 { normalizePhoneNumber, resolveMaybeDynamic } from '../../utils/misc-utils'
|
||||
|
||||
// @available=both
|
||||
/**
|
||||
* Start the client in an interactive and declarative manner,
|
||||
* by providing callbacks for authorization details.
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { tl } from '@mtcute/core'
|
||||
import { Long } from '@mtcute/core'
|
||||
|
||||
import { TelegramClient } from '../../client'
|
||||
|
||||
// @available=bot
|
||||
/**
|
||||
* Send an answer to a callback query.
|
||||
*
|
||||
|
@ -11,7 +12,7 @@ import { TelegramClient } from '../../client'
|
|||
*/
|
||||
export async function answerCallbackQuery(
|
||||
this: TelegramClient,
|
||||
queryId: tl.Long,
|
||||
queryId: Long,
|
||||
params?: {
|
||||
/**
|
||||
* 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_PREMIUM = 8000 // 512 kb * 8000 = 4000 MiB
|
||||
|
||||
// @available=both
|
||||
/**
|
||||
* Upload a file to Telegram servers, without actually
|
||||
* sending a message anywhere. Useful when an `InputFile` is required.
|
||||
|
|
|
@ -41,6 +41,7 @@ export async function getMessages(
|
|||
fromReply?: boolean,
|
||||
): Promise<(Message | null)[]>
|
||||
|
||||
// @available=both
|
||||
/** @internal */
|
||||
export async function getMessages(
|
||||
this: TelegramClient,
|
||||
|
|
|
@ -5,6 +5,7 @@ import { TelegramClient } from '../../client'
|
|||
import { InputPeerLike, MtPeerNotFoundError } from '../../types'
|
||||
import { normalizeToInputPeer } from '../../utils/peer-utils'
|
||||
|
||||
// @available=both
|
||||
/**
|
||||
* Get the `InputPeer` of a known peer id.
|
||||
* Useful when an `InputPeer` is needed.
|
||||
|
|
Loading…
Reference in a new issue