mtcute/packages/tl-reference/scripts/fetch-history.js
2021-04-14 18:08:53 +03:00

388 lines
12 KiB
JavaScript

const fs = require('fs')
const path = require('path')
const { convertTlToJson } = require('../../tl/scripts/generate-schema')
const fetch = require('node-fetch')
const qs = require('querystring')
const { convertToArrays } = require('./prepare-data')
const UNIX_0 = '1970-01-01T00:00:00Z'
const CURRENT_FILE = 'Telegram/Resources/tl/api.tl'
const FILES = [
'Telegram/SourceFiles/mtproto/scheme.tl',
'Telegram/Resources/scheme.tl',
CURRENT_FILE,
]
async function getLastFetched() {
return fs.promises
.readFile(path.join(__dirname, '../data/history/last-fetched.json'), 'utf8')
.then((res) => JSON.parse(res))
.catch(() => ({
...FILES.reduce((a, b) => {
a[b] = UNIX_0
return a
}, {}),
}))
}
async function updateLastFetched(file, time) {
return getLastFetched().then((state) =>
fs.promises.writeFile(
path.join(__dirname, '../data/history/last-fetched.json'),
JSON.stringify({
...state,
[file]: time,
})
)
)
}
async function getFileContent(file, commit) {
return fetch(
`https://raw.githubusercontent.com/telegramdesktop/tdesktop/${commit}/${file}`
).then((r) => r.text())
}
async function parseRemoteTl(file, commit) {
let content = await getFileContent(file, commit)
if (content === '404: Not Found') return null
let layer = (function () {
const m = content.match(/^\/\/ LAYER (\d+)/m)
if (m) return m[1]
return null
})()
if (!layer) {
// older files did not contain layer number in comment.
if (content.match(/invokeWithLayer#da9b0d0d/)) {
// if this is present, then the layer number is available in
// Telegram/SourceFiles/mtproto/mtpCoreTypes.h
let mtpCoreTypes = await getFileContent(
'Telegram/SourceFiles/mtproto/mtpCoreTypes.h',
commit
)
if (mtpCoreTypes === '404: Not Found') {
mtpCoreTypes = await getFileContent(
'Telegram/SourceFiles/mtproto/core_types.h',
commit
)
}
const m = mtpCoreTypes.match(
/^static const mtpPrime mtpCurrentLayer = (\d+);$/m
)
if (!m)
throw new Error(
`Could not determine layer number for file ${file} at commit ${commit}`
)
layer = m[1]
} else {
// even older files on ancient layers
// layer number is the largest available invokeWithLayerN constructor
let max = 0
content.replace(/invokeWithLayer(\d+)#[0-f]+/g, (_, $1) => {
$1 = parseInt($1)
if ($1 > max) max = $1
})
if (max === 0)
throw new Error(
`Could not determine layer number for file ${file} at commit ${commit}`
)
layer = max + ''
}
}
if (content.match(/bad_server_salt#/)) {
// this is an older file that contained both mtproto and api
// since we are only interested in api, remove the mtproto part
const lines = content.split('\n')
const apiIdx = lines.indexOf('///////// Main application API')
if (apiIdx === -1)
throw new Error('Could not find split point for combined file')
content = lines.slice(apiIdx).join('\n')
}
return {
layer,
content,
tl: await convertTlToJson(content, 'api', true),
}
}
function createTlDifference(old, mod) {
const diff = {
added: { classes: [], methods: [], unions: [] },
removed: { classes: [], methods: [], unions: [] },
modified: { classes: [], methods: [], unions: [] },
}
old = convertToArrays(old.tl)
mod = convertToArrays(mod.tl)
// create index for both old and mod
const { oldIndex, modIndex } = (function () {
function createIndex(it) {
let ret = {}
it.classes.forEach((obj) => {
obj.uid = 'c_' + obj.name
obj._type = 'classes'
ret[obj.uid] = obj
})
it.methods.forEach((obj) => {
obj.uid = 'm_' + obj.name
obj._type = 'methods'
ret[obj.uid] = obj
})
it.unions.forEach((obj) => {
obj.uid = 'u_' + obj.type
obj._type = 'unions'
ret[obj.uid] = obj
})
return ret
}
return {
oldIndex: createIndex(old),
modIndex: createIndex(mod),
}
})()
// find difference between constructor arguments
function createArgsDifference(old, mod) {
const diff = {
added: [],
removed: [],
modified: [],
}
const { oldIndex, modIndex } = (function () {
function createIndex(obj) {
const ret = {}
if (obj.arguments)
obj.arguments.forEach((arg) => (ret[arg.name] = arg))
return ret
}
return {
oldIndex: createIndex(old),
modIndex: createIndex(mod),
}
})()
Object.keys(modIndex).forEach((argName) => {
if (!(argName in oldIndex)) {
diff.added.push(modIndex[argName])
} else {
const old = oldIndex[argName]
const mod = modIndex[argName]
if (
old.type !== mod.type ||
old.optional !== mod.optional ||
mod.predicate !== mod.predicate
) {
diff.modified.push({
name: argName,
old: old,
new: mod,
})
}
}
})
Object.keys(oldIndex).forEach((argName) => {
if (!(argName in modIndex)) {
diff.removed.push(oldIndex[argName])
}
})
return diff
}
Object.keys(modIndex).forEach((uid) => {
if (!(uid in oldIndex)) {
diff.added[modIndex[uid]._type].push(modIndex[uid])
} else {
const old = oldIndex[uid]
const mod = modIndex[uid]
const localDiff = {}
const argDiff = createArgsDifference(old, mod)
if (
argDiff.removed.length ||
argDiff.added.length ||
argDiff.modified.length
) {
localDiff.arguments = argDiff
}
if (old.id !== mod.id) localDiff.id = { old: old.id, new: mod.id }
if (old.type !== mod.type)
localDiff.type = { old: old.type, new: mod.type }
if (old.returns !== mod.returns)
localDiff.returns = { old: old.returns, new: mod.returns }
if (Object.keys(localDiff).length) {
localDiff.name = old.name
diff.modified[oldIndex[uid]._type].push(localDiff)
}
}
})
Object.keys(oldIndex).forEach((uid) => {
if (!(uid in modIndex)) {
diff.removed[oldIndex[uid]._type].push(oldIndex[uid])
}
})
return diff
}
function fileSafeDateFormat(date) {
date = new Date(date)
return date.toISOString().replace(/[\-:]|\.\d\d\d/g, '')
}
function shortSha(sha) {
return sha.substr(0, 7)
}
async function fetchHistory(file, since, defaultParent = null) {
const history = await (async function () {
const ret = []
let page = 1
while (true) {
const chunk = await fetch(
`https://api.github.com/repos/telegramdesktop/tdesktop/commits?` +
qs.stringify({
since,
path: file,
per_page: 100,
page,
})
).then((r) => r.json())
if (!chunk.length) break
ret.push(...chunk)
page += 1
}
return ret
})()
// should not happen
if (history.length === 0) throw new Error('history is empty')
const filename = (schema, commit) =>
`layer${schema.layer}-${fileSafeDateFormat(
commit.commit.committer.date
)}-${shortSha(commit.sha)}.json`
function writeSchemaToFile(schema, commit) {
return fs.promises.writeFile(
path.join(__dirname, `../data/history/${filename(schema, commit)}`),
JSON.stringify({
tl: JSON.stringify(schema.tl),
layer: parseInt(schema.layer),
content: schema.content,
// idk where parent: '00' comes from but whatever
parent: schema.parent && schema.parent !== '00' ? schema.parent : defaultParent,
source: {
file,
date: commit.commit.committer.date,
commit: commit.sha,
message: commit.message,
},
})
)
}
let base = history.pop()
let baseSchema = await parseRemoteTl(file, base.sha)
let baseFilename = () => filename(baseSchema, base)
try {
await fs.promises.access(
path.join(__dirname, `../data/history/${baseFilename()}`),
fs.F_OK
)
} catch (e) {
await writeSchemaToFile(baseSchema, base)
}
while (history.length) {
const next = history.pop()
const nextSchema = await parseRemoteTl(file, next.sha)
if (!nextSchema) break
const diff = createTlDifference(baseSchema, nextSchema)
await fs.promises.writeFile(
path.join(
__dirname,
`../data/diffs/${shortSha(base.sha)}-${shortSha(next.sha)}.json`
),
JSON.stringify({
...diff,
// yeah they sometimes update schema w/out changing layer number
layer:
baseSchema.layer === nextSchema.layer
? undefined
: nextSchema.layer,
})
)
nextSchema.parent = baseFilename()
base = next
baseSchema = nextSchema
await updateLastFetched(file, base.commit.committer.date)
await writeSchemaToFile(baseSchema, base)
console.log(
'Fetched commit %s, file %s (%s)',
shortSha(base.sha),
file,
base.commit.committer.date
)
}
if (file !== CURRENT_FILE) {
await updateLastFetched(file, 'DONE:' + baseFilename())
}
console.log('No more commits for %s', file)
}
async function main() {
const last = await getLastFetched()
for (let i = 0; i < FILES.length; i++) {
const file = FILES[i]
const prev = FILES[i - 1]
if (!last[file].startsWith('DONE')) {
let parent = prev ? last[prev].split(':')[1] : null
await fetchHistory(file, last[file], parent)
}
}
console.log('Creating reverse links ("next" field)')
for (const file of await fs.promises.readdir(
path.join(__dirname, '../data/history')
)) {
if (!file.startsWith('layer')) continue
const fullPath = path.join(__dirname, '../data/history', file)
const json = JSON.parse(await fs.promises.readFile(fullPath, 'utf-8'))
if (json.parent) {
const parentPath = path.join(__dirname, '../data/history', json.parent)
const parentJson = JSON.parse(
await fs.promises.readFile(
parentPath,
'utf-8'
)
)
parentJson.next = parentPath
await fs.promises.writeFile(parentPath, JSON.stringify(parentJson))
}
}
}
main().catch(console.error)