2021-11-23 00:03:59 +03:00
|
|
|
// Downloads latest .tl schemas from various sources,
|
|
|
|
// fetches documentation from https://corefork.telegram.org/schema
|
|
|
|
// and builds a single .json file from all of that
|
|
|
|
//
|
|
|
|
// Conflicts merging is interactive, so we can't put this in CI
|
|
|
|
|
2024-08-13 04:53:07 +03:00
|
|
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
|
|
import { join } from 'node:path'
|
|
|
|
import * as readline from 'node:readline'
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2024-08-13 04:53:07 +03:00
|
|
|
import * as cheerio from 'cheerio'
|
2024-08-19 14:08:49 +03:00
|
|
|
import { isPresent } from '@mtcute/core/utils.js'
|
2024-08-13 04:53:07 +03:00
|
|
|
import type {
|
|
|
|
TlEntry,
|
|
|
|
TlFullSchema,
|
|
|
|
} from '@mtcute/tl-utils'
|
2022-08-12 20:11:27 +03:00
|
|
|
import {
|
2023-06-05 03:30:48 +03:00
|
|
|
generateTlSchemasDifference,
|
2022-08-12 20:11:27 +03:00
|
|
|
mergeTlEntries,
|
|
|
|
mergeTlSchemas,
|
2023-06-05 03:30:48 +03:00
|
|
|
parseFullTlSchema,
|
|
|
|
parseTlToEntries,
|
2022-08-12 20:11:27 +03:00
|
|
|
writeTlEntryToString,
|
|
|
|
} from '@mtcute/tl-utils'
|
2024-03-07 05:46:04 +03:00
|
|
|
import { parseTlEntriesFromJson } from '@mtcute/tl-utils/json.js'
|
2024-11-19 18:35:02 +03:00
|
|
|
import { ffetch } from '@fuman/fetch'
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
import {
|
2023-06-05 03:30:48 +03:00
|
|
|
API_SCHEMA_DIFF_JSON_FILE,
|
2022-04-28 17:23:44 +03:00
|
|
|
API_SCHEMA_JSON_FILE,
|
2023-06-05 03:30:48 +03:00
|
|
|
BLOGFORK_DOMAIN,
|
|
|
|
COREFORK_DOMAIN,
|
2024-08-13 04:53:07 +03:00
|
|
|
CORE_DOMAIN,
|
2023-07-20 17:51:24 +03:00
|
|
|
TDESKTOP_LAYER,
|
2022-04-28 17:23:44 +03:00
|
|
|
TDESKTOP_SCHEMA,
|
|
|
|
TDLIB_SCHEMA,
|
2024-03-07 05:46:04 +03:00
|
|
|
WEBA_LAYER,
|
|
|
|
WEBA_SCHEMA,
|
|
|
|
WEBK_SCHEMA,
|
2024-08-13 04:53:07 +03:00
|
|
|
__dirname,
|
2023-10-16 19:23:53 +03:00
|
|
|
} from './constants.js'
|
|
|
|
import { applyDocumentation, fetchDocumentation, getCachedDocumentation } from './documentation.js'
|
2024-08-13 04:53:07 +03:00
|
|
|
import type { TlPackedSchema } from './schema.js'
|
|
|
|
import { packTlSchema, unpackTlSchema } from './schema.js'
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const README_MD_FILE = join(__dirname, '../README.md')
|
|
|
|
const PACKAGE_JSON_FILE = join(__dirname, '../package.json')
|
|
|
|
|
|
|
|
function tlToFullSchema(tl: string): TlFullSchema {
|
2023-07-20 22:07:07 +03:00
|
|
|
return parseFullTlSchema(
|
|
|
|
parseTlToEntries(tl, {
|
|
|
|
parseMethodTypes: true,
|
|
|
|
}),
|
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
interface Schema {
|
|
|
|
name: string
|
|
|
|
layer: number
|
|
|
|
content: TlFullSchema
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchTdlibSchema(): Promise<Schema> {
|
2024-11-19 18:35:02 +03:00
|
|
|
const schema = await ffetch(TDLIB_SCHEMA).text()
|
|
|
|
const versionHtml = await ffetch('https://raw.githubusercontent.com/tdlib/td/master/td/telegram/Version.h').text()
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
const layer = versionHtml.match(/^constexpr int32 MTPROTO_LAYER = (\d+)/m)
|
|
|
|
if (!layer) throw new Error('Layer number not available')
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: 'TDLib',
|
2024-08-13 04:53:07 +03:00
|
|
|
layer: Number.parseInt(layer[1]),
|
2021-11-23 00:03:59 +03:00
|
|
|
content: tlToFullSchema(schema),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchTdesktopSchema(): Promise<Schema> {
|
2024-11-19 18:35:02 +03:00
|
|
|
const schema = await ffetch(TDESKTOP_SCHEMA).text()
|
|
|
|
const layerFile = await ffetch(TDESKTOP_LAYER).text()
|
2023-07-20 17:51:24 +03:00
|
|
|
const layer = `${schema}\n\n${layerFile}`.match(/^\/\/ LAYER (\d+)/m)
|
2021-11-23 00:03:59 +03:00
|
|
|
if (!layer) throw new Error('Layer number not available')
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: 'TDesktop',
|
2024-08-13 04:53:07 +03:00
|
|
|
layer: Number.parseInt(layer[1]),
|
2021-11-23 00:03:59 +03:00
|
|
|
content: tlToFullSchema(schema),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-24 01:32:22 +03:00
|
|
|
async function fetchCoreSchema(domain = CORE_DOMAIN, name = 'Core'): Promise<Schema> {
|
2024-11-19 18:35:02 +03:00
|
|
|
const html = await ffetch(`${domain}/schema`).text()
|
2021-11-23 00:03:59 +03:00
|
|
|
const $ = cheerio.load(html)
|
|
|
|
// cheerio doesn't always unescape them
|
2023-09-24 01:32:22 +03:00
|
|
|
const schema = $('.page_scheme code').text().replace(/</g, '<').replace(/>/g, '>')
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
const layer = $('.dev_layer_select .dropdown-toggle')
|
|
|
|
.text()
|
|
|
|
.trim()
|
|
|
|
.match(/^Layer (\d+)$/i)
|
|
|
|
if (!layer) throw new Error('Layer number not available')
|
|
|
|
|
|
|
|
return {
|
2022-04-01 22:17:10 +03:00
|
|
|
name,
|
2024-08-13 04:53:07 +03:00
|
|
|
layer: Number.parseInt(layer[1]),
|
2021-11-23 00:03:59 +03:00
|
|
|
content: tlToFullSchema(schema),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-07 05:46:04 +03:00
|
|
|
async function fetchWebkSchema(): Promise<Schema> {
|
2024-11-19 18:35:02 +03:00
|
|
|
const schema = await ffetch(WEBK_SCHEMA).text()
|
2024-03-07 05:46:04 +03:00
|
|
|
const json = JSON.parse(schema) as {
|
|
|
|
layer: number
|
|
|
|
API: object
|
|
|
|
}
|
|
|
|
|
|
|
|
let entries = parseTlEntriesFromJson(json.API, { parseMethodTypes: true })
|
|
|
|
entries = entries.filter((it) => {
|
|
|
|
if (it.kind === 'method') {
|
|
|
|
// json schema doesn't provide info about generics, remove these
|
2024-08-13 04:53:07 +03:00
|
|
|
return !it.arguments.some(arg => arg.type === '!X') && it.type !== 'X'
|
2024-03-07 05:46:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: 'WebK',
|
|
|
|
layer: json.layer,
|
|
|
|
content: parseFullTlSchema(entries),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function fetchWebaSchema(): Promise<Schema> {
|
2024-11-19 18:35:02 +03:00
|
|
|
const [schema, layerFile] = await Promise.all([
|
|
|
|
ffetch(WEBA_SCHEMA).text(),
|
|
|
|
ffetch(WEBA_LAYER).text(),
|
|
|
|
])
|
2024-03-07 05:46:04 +03:00
|
|
|
|
|
|
|
// const LAYER = 174;
|
|
|
|
const version = layerFile.match(/^const LAYER = (\d+);$/m)
|
|
|
|
if (!version) throw new Error('Layer number not found')
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: 'WebA',
|
2024-08-13 04:53:07 +03:00
|
|
|
layer: Number.parseInt(version[1]),
|
2024-03-07 05:46:04 +03:00
|
|
|
content: tlToFullSchema(schema),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
function input(rl: readline.Interface, q: string): Promise<string> {
|
2024-08-13 04:53:07 +03:00
|
|
|
return new Promise(resolve => rl.question(q, resolve))
|
2021-11-23 00:03:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
interface ConflictOption {
|
|
|
|
schema: Schema
|
|
|
|
entry?: TlEntry
|
|
|
|
}
|
|
|
|
|
|
|
|
async function updateReadme(currentLayer: number) {
|
|
|
|
const oldReadme = await readFile(README_MD_FILE, 'utf8')
|
|
|
|
const today = new Date().toLocaleDateString('ru')
|
|
|
|
await writeFile(
|
|
|
|
README_MD_FILE,
|
|
|
|
oldReadme.replace(
|
|
|
|
/^Generated from TL layer \*\*\d+\*\* \(last updated on \d+\.\d+\.\d+\)\.$/m,
|
2023-06-05 03:30:48 +03:00
|
|
|
`Generated from TL layer **${currentLayer}** (last updated on ${today}).`,
|
|
|
|
),
|
2021-11-23 00:03:59 +03:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-09-24 01:32:22 +03:00
|
|
|
async function updatePackageVersion(rl: readline.Interface, currentLayer: number) {
|
|
|
|
const packageJson = JSON.parse(await readFile(PACKAGE_JSON_FILE, 'utf8')) as { version: string }
|
2023-09-03 02:37:51 +03:00
|
|
|
const version = packageJson.version
|
2024-08-13 04:53:07 +03:00
|
|
|
let [major, minor] = version.split('.').map(i => Number.parseInt(i))
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
if (major === currentLayer) {
|
|
|
|
console.log('Current version: %s. Bump minor version?', version)
|
|
|
|
const res = await input(rl, '[Y/n] > ')
|
|
|
|
|
|
|
|
if (res.trim().toLowerCase() === 'n') {
|
|
|
|
return
|
|
|
|
}
|
2022-06-05 23:05:15 +03:00
|
|
|
|
|
|
|
minor += 1
|
2021-11-23 00:03:59 +03:00
|
|
|
} else {
|
|
|
|
major = currentLayer
|
|
|
|
minor = 0
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log('Updating package version...')
|
2022-04-28 17:23:44 +03:00
|
|
|
const versionStr = `${major}.${minor}.0`
|
|
|
|
packageJson.version = versionStr
|
2021-11-23 00:03:59 +03:00
|
|
|
await writeFile(PACKAGE_JSON_FILE, JSON.stringify(packageJson, null, 4))
|
|
|
|
}
|
|
|
|
|
|
|
|
async function overrideInt53(schema: TlFullSchema): Promise<void> {
|
|
|
|
console.log('Applying int53 overrides...')
|
|
|
|
|
2023-09-24 01:32:22 +03:00
|
|
|
const config = JSON.parse(await readFile(join(__dirname, '../data/int53-overrides.json'), 'utf8')) as Record<
|
|
|
|
string,
|
|
|
|
Record<string, string[]>
|
|
|
|
>
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
schema.entries.forEach((entry) => {
|
|
|
|
const overrides: string[] | undefined = config[entry.kind][entry.name]
|
|
|
|
if (!overrides) return
|
|
|
|
|
|
|
|
overrides.forEach((argName) => {
|
2024-08-13 04:53:07 +03:00
|
|
|
const arg = entry.arguments.find(it => it.name === argName)
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
if (!arg) {
|
2023-09-24 01:32:22 +03:00
|
|
|
console.log(`[warn] Cannot override ${entry.name}#${argName}: argument does not exist`)
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (arg.type === 'long') {
|
|
|
|
arg.type = 'int53'
|
|
|
|
} else if (arg.type.toLowerCase() === 'vector<long>') {
|
|
|
|
arg.type = 'vector<int53>'
|
|
|
|
} else {
|
2023-09-24 01:32:22 +03:00
|
|
|
console.log(`[warn] Cannot override ${entry.name}#${argName}: argument is not long (${arg.type})`)
|
2021-11-23 00:03:59 +03:00
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async function main() {
|
|
|
|
console.log('Loading schemas...')
|
|
|
|
|
2024-03-07 05:46:04 +03:00
|
|
|
const schemas: Schema[] = await Promise.all([
|
|
|
|
fetchTdlibSchema(),
|
|
|
|
fetchTdesktopSchema(),
|
|
|
|
fetchCoreSchema(),
|
|
|
|
fetchCoreSchema(COREFORK_DOMAIN, 'Corefork'),
|
|
|
|
fetchCoreSchema(BLOGFORK_DOMAIN, 'Blogfork'),
|
|
|
|
fetchWebkSchema(),
|
|
|
|
fetchWebaSchema(),
|
2024-08-13 04:53:07 +03:00
|
|
|
readFile(join(__dirname, '../data/custom.tl'), 'utf8').then(tl => ({
|
2021-11-23 00:03:59 +03:00
|
|
|
name: 'Custom',
|
|
|
|
layer: 0, // handled manually
|
2024-03-07 05:46:04 +03:00
|
|
|
content: tlToFullSchema(tl),
|
|
|
|
})),
|
|
|
|
])
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
console.log('Available schemas:')
|
2024-08-13 04:53:07 +03:00
|
|
|
schemas.forEach(schema =>
|
2023-09-24 01:32:22 +03:00
|
|
|
console.log(' - %s (layer %d): %d entries', schema.name, schema.layer, schema.content.entries.length),
|
2021-11-23 00:03:59 +03:00
|
|
|
)
|
|
|
|
|
2024-08-13 04:53:07 +03:00
|
|
|
const resultLayer = Math.max(...schemas.map(it => it.layer))
|
2021-11-23 00:03:59 +03:00
|
|
|
console.log(`Final schema will be on layer ${resultLayer}. Merging...`)
|
|
|
|
|
|
|
|
const rl = readline.createInterface({
|
|
|
|
input: process.stdin,
|
|
|
|
output: process.stdout,
|
|
|
|
})
|
|
|
|
|
|
|
|
const resultSchema = await mergeTlSchemas(
|
2024-08-13 04:53:07 +03:00
|
|
|
schemas.map(it => it.content),
|
2021-11-23 00:03:59 +03:00
|
|
|
async (_options) => {
|
|
|
|
const options: ConflictOption[] = _options.map((it, idx) => ({
|
|
|
|
schema: schemas[idx],
|
|
|
|
entry: it,
|
|
|
|
}))
|
|
|
|
|
|
|
|
let chooseOptions: ConflictOption[] = []
|
2023-09-24 19:56:13 +03:00
|
|
|
let mergeError = ''
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
const customEntry = options[options.length - 1]
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
if (customEntry.entry) {
|
|
|
|
// if there is custom entry in conflict, we must present it, otherwise something may go wrong
|
|
|
|
chooseOptions = options
|
|
|
|
} else {
|
|
|
|
// first of all, prefer entries from the latest layer
|
2024-08-13 04:53:07 +03:00
|
|
|
let fromLastSchema = options.filter(opt => opt.entry && opt.schema.layer === resultLayer)
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
// if there is only one schema on the latest layer, we can simply return it
|
|
|
|
if (fromLastSchema.length === 1) return fromLastSchema[0].entry
|
|
|
|
|
2023-10-29 09:14:48 +03:00
|
|
|
// the conflict was earlier, and now this entry is removed altogether.
|
|
|
|
// keep it just in case for now, as it may still be referenced somewhere
|
|
|
|
if (fromLastSchema.length === 0) {
|
2024-08-13 04:53:07 +03:00
|
|
|
fromLastSchema = options.sort((a, b) => b.schema.layer - a.schema.layer).filter(opt => opt.entry)
|
2023-10-29 09:14:48 +03:00
|
|
|
// only keep the latest item
|
|
|
|
fromLastSchema = [fromLastSchema[0]]
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
// there are multiple choices on the latest layer
|
|
|
|
// if they are all the same, it's just conflict between layers,
|
|
|
|
// and we can merge the ones from the latest layer
|
2024-08-13 04:53:07 +03:00
|
|
|
const mergedEntry = mergeTlEntries(fromLastSchema.map(opt => opt.entry).filter(isPresent))
|
2021-11-23 00:03:59 +03:00
|
|
|
if (typeof mergedEntry === 'string') {
|
|
|
|
// merge failed, so there is in fact some conflict
|
|
|
|
chooseOptions = fromLastSchema
|
2023-09-24 19:56:13 +03:00
|
|
|
mergeError = mergedEntry
|
2024-08-13 04:53:07 +03:00
|
|
|
} else {
|
|
|
|
return mergedEntry
|
|
|
|
}
|
2021-11-23 00:03:59 +03:00
|
|
|
}
|
|
|
|
|
2024-08-19 14:08:49 +03:00
|
|
|
const nonEmptyOptions = chooseOptions.filter(it => it.entry !== undefined)
|
2021-11-23 00:03:59 +03:00
|
|
|
|
2023-09-24 19:56:13 +03:00
|
|
|
console.log(
|
|
|
|
'Conflict detected (%s) at %s %s:',
|
|
|
|
mergeError,
|
2024-08-19 14:08:49 +03:00
|
|
|
nonEmptyOptions[0].entry!.kind,
|
|
|
|
nonEmptyOptions[0].entry!.name,
|
2023-09-24 19:56:13 +03:00
|
|
|
)
|
2021-11-23 00:03:59 +03:00
|
|
|
console.log('0. Remove')
|
|
|
|
nonEmptyOptions.forEach((opt, idx) => {
|
2024-08-19 14:08:49 +03:00
|
|
|
console.log(`${idx + 1}. ${opt.schema.name}: (${opt.entry!.kind}) ${writeTlEntryToString(opt.entry!)}`)
|
2021-11-23 00:03:59 +03:00
|
|
|
})
|
|
|
|
|
|
|
|
while (true) {
|
2024-08-13 04:53:07 +03:00
|
|
|
const res = Number.parseInt(await input(rl, `[0-${nonEmptyOptions.length}] > `))
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2024-08-13 04:53:07 +03:00
|
|
|
if (Number.isNaN(res) || res < 0 || res > nonEmptyOptions.length) {
|
2023-07-20 17:51:24 +03:00
|
|
|
continue
|
|
|
|
}
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
if (res === 0) return undefined
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
return nonEmptyOptions[res - 1].entry
|
|
|
|
}
|
2023-06-05 03:30:48 +03:00
|
|
|
},
|
2021-11-23 00:03:59 +03:00
|
|
|
)
|
|
|
|
|
2023-09-24 01:32:22 +03:00
|
|
|
console.log('Done! Final schema contains %d entries', resultSchema.entries.length)
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
let docs = await getCachedDocumentation()
|
|
|
|
|
|
|
|
if (docs) {
|
|
|
|
console.log('Cached documentation from %s, use it?', docs.updated)
|
|
|
|
const res = await input(rl, '[Y/n] > ')
|
|
|
|
|
|
|
|
if (res.trim().toLowerCase() === 'n') {
|
|
|
|
docs = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (docs === null) {
|
|
|
|
console.log('Downloading documentation...')
|
|
|
|
docs = await fetchDocumentation(resultSchema, resultLayer)
|
|
|
|
}
|
|
|
|
|
|
|
|
applyDocumentation(resultSchema, docs)
|
|
|
|
|
|
|
|
await overrideInt53(resultSchema)
|
|
|
|
|
2022-10-30 18:38:31 +03:00
|
|
|
console.log('Writing diff to file...')
|
2023-09-24 01:32:22 +03:00
|
|
|
const oldSchema = unpackTlSchema(JSON.parse(await readFile(API_SCHEMA_JSON_FILE, 'utf8')) as TlPackedSchema)
|
2022-10-30 18:38:31 +03:00
|
|
|
await writeFile(
|
|
|
|
API_SCHEMA_DIFF_JSON_FILE,
|
2023-07-20 17:51:24 +03:00
|
|
|
JSON.stringify(
|
|
|
|
{
|
|
|
|
layer: [oldSchema[1], resultLayer],
|
|
|
|
diff: generateTlSchemasDifference(oldSchema[0], resultSchema),
|
|
|
|
},
|
|
|
|
null,
|
|
|
|
4,
|
|
|
|
),
|
2022-10-30 18:38:31 +03:00
|
|
|
)
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
console.log('Writing result to file...')
|
2023-09-24 01:32:22 +03:00
|
|
|
await writeFile(API_SCHEMA_JSON_FILE, JSON.stringify(packTlSchema(resultSchema, resultLayer)))
|
2021-11-23 00:03:59 +03:00
|
|
|
|
|
|
|
console.log('Updating README.md...')
|
|
|
|
await updateReadme(resultLayer)
|
|
|
|
|
|
|
|
await updatePackageVersion(rl, resultLayer)
|
|
|
|
|
|
|
|
rl.close()
|
|
|
|
|
|
|
|
console.log('Done!')
|
|
|
|
}
|
|
|
|
|
|
|
|
main().catch(console.error)
|