diff --git a/packages/tl-reference/data/README.md b/packages/tl-reference/data/README.md index d4b69c32..427a8947 100644 --- a/packages/tl-reference/data/README.md +++ b/packages/tl-reference/data/README.md @@ -3,4 +3,14 @@ This directory contains historical data about the TL schema. That is, it contains history of the schema over time and pre-calculated difference between consecutive versions. -The files are generated with `fetch-history.js` script and are not in the repository. +The files are generated with scripts and are not in the repository. + +To generate the files, execute scripts from `../scripts`: +1. `fetch-history.js` +2. `fetch-older-layers.js` +3. `generate-type-history.js` + +To update files: +1. `fetch-history.js` +2. `generate-type-history.js` + diff --git a/packages/tl-reference/data/history/.gitignore b/packages/tl-reference/data/history/.gitignore index a6c57f5f..35340fb5 100644 --- a/packages/tl-reference/data/history/.gitignore +++ b/packages/tl-reference/data/history/.gitignore @@ -1 +1,2 @@ *.json +*.txt diff --git a/packages/tl-reference/data/types/.gitignore b/packages/tl-reference/data/types/.gitignore new file mode 100644 index 00000000..a6c57f5f --- /dev/null +++ b/packages/tl-reference/data/types/.gitignore @@ -0,0 +1 @@ +*.json diff --git a/packages/tl-reference/gatsby-node.js b/packages/tl-reference/gatsby-node.js index 774ba6c4..92c73aad 100644 --- a/packages/tl-reference/gatsby-node.js +++ b/packages/tl-reference/gatsby-node.js @@ -71,6 +71,9 @@ exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { const TLObject = path.resolve('./src/templates/tl-object.tsx') const TlTypesList = path.resolve('./src/templates/tl-types-list.tsx') +const TypeHistory = path.resolve('./src/templates/type-history.tsx') +const TlLayer = path.resolve('./src/templates/tl-layer.tsx') +const TlDiff = path.resolve('./src/templates/tl-diff.tsx') exports.createPages = async ({ graphql, actions }) => { const result = await graphql(` @@ -84,6 +87,23 @@ exports.createPages = async ({ graphql, actions }) => { subtypes } } + + allTypesJson { + nodes { + uid + name + type + } + } + + allHistoryJson { + nodes { + layer + rev + prev + next + } + } } `) @@ -98,6 +118,22 @@ exports.createPages = async ({ graphql, actions }) => { }) }) + result.data.allTypesJson.nodes.forEach((node) => { + actions.createPage({ + path: `/history/${node.type}/${node.name}`, + component: TypeHistory, + context: node + }) + }) + + result.data.allHistoryJson.nodes.forEach((node) => { + actions.createPage({ + path: `/history/layer${node.layer}${node.rev > 0 ? `-rev${node.rev}` : ''}`, + component: TlLayer, + context: node + }) + }) + const result2 = await graphql(` query { allTlObject { @@ -112,14 +148,13 @@ exports.createPages = async ({ graphql, actions }) => { `) result2.data.allTlObject.group.forEach(({ fieldValue: prefix, nodes }) => { - const namespaces = [...new Set(nodes.map(i => i.namespace))] + const namespaces = [...new Set(nodes.map((i) => i.namespace))] namespaces.forEach((ns) => { let namespace if (ns === '$root') namespace = '' else namespace = '/' + ns - - ;(['types', 'methods']).forEach((type) => { + ;['types', 'methods'].forEach((type) => { actions.createPage({ path: `${prefix}${type}${namespace}`, component: TlTypesList, @@ -128,8 +163,8 @@ exports.createPages = async ({ graphql, actions }) => { ns, type, isTypes: type === 'types', - isMethods: type === 'methods' - } + isMethods: type === 'methods', + }, }) }) }) diff --git a/packages/tl-reference/scripts/diff-utils.js b/packages/tl-reference/scripts/diff-utils.js new file mode 100644 index 00000000..5967c575 --- /dev/null +++ b/packages/tl-reference/scripts/diff-utils.js @@ -0,0 +1,180 @@ +// in js because also used in scripts + +function createTlSchemaIndex(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 +} + +function createTlConstructorDifference(old, mod) { + const localDiff = {} + + const argDiff = { + 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)) { + argDiff.added.push(modIndex[argName]) + } else { + const old = oldIndex[argName] + const mod = modIndex[argName] + if ( + old.type !== mod.type || + old.optional !== mod.optional || + old.predicate !== mod.predicate + ) { + argDiff.modified.push({ + name: argName, + old: old, + new: mod, + }) + } + } + }) + + Object.keys(oldIndex).forEach((argName) => { + if (!(argName in modIndex)) { + argDiff.removed.push(oldIndex[argName]) + } + }) + + 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) return localDiff + return null +} + +function createTlUnionsDifference(old, mod) { + const diff = { + added: [], + removed: [], + } + + const { oldIndex, modIndex } = (function () { + function createIndex(obj) { + const ret = {} + obj.subtypes.forEach((typ) => (ret[typ] = 1)) + return ret + } + + return { + oldIndex: createIndex(old), + modIndex: createIndex(mod), + } + })() + + Object.keys(modIndex).forEach((typ) => { + if (!(typ in oldIndex)) { + diff.added.push(typ) + } + }) + + Object.keys(oldIndex).forEach((typ) => { + if (!(typ in modIndex)) { + diff.removed.push(typ) + } + }) + + if (diff.added.length || diff.removed.length) { + return { subtypes: diff } + } + + return null +} + +function createTlSchemaDifference(old, mod) { + const diff = { + added: { classes: [], methods: [], unions: [] }, + removed: { classes: [], methods: [], unions: [] }, + modified: { classes: [], methods: [], unions: [] }, + } + + old = old.tl + mod = mod.tl + + // create index for both old and mod + const oldIndex = createTlSchemaIndex(old) + const modIndex = createTlSchemaIndex(mod) + + Object.keys(modIndex).forEach((uid) => { + const type = modIndex[uid]._type + + if (!(uid in oldIndex)) { + diff.added[type].push(modIndex[uid]) + } else { + const old = oldIndex[uid] + const mod = modIndex[uid] + + let localDiff + if (type === 'unions') { + localDiff = createTlUnionsDifference(old, mod) + } else { + localDiff = createTlConstructorDifference(old, mod) + } + + if (localDiff) { + localDiff.name = old.name || old.type + diff.modified[type].push(localDiff) + } + } + }) + + Object.keys(oldIndex).forEach((uid) => { + if (!(uid in modIndex)) { + diff.removed[oldIndex[uid]._type].push(oldIndex[uid]) + } + }) + + return diff +} + +module.exports = { + createTlSchemaIndex, + createTlConstructorDifference, + createTlUnionsDifference, + createTlSchemaDifference, +} diff --git a/packages/tl-reference/scripts/fetch-history.js b/packages/tl-reference/scripts/fetch-history.js index 7ae7e4da..ec620de1 100644 --- a/packages/tl-reference/scripts/fetch-history.js +++ b/packages/tl-reference/scripts/fetch-history.js @@ -15,20 +15,23 @@ const FILES = [ async function getLastFetched() { return fs.promises - .readFile(path.join(__dirname, '../data/history/last-fetched.json'), 'utf8') + .readFile( + path.join(__dirname, '../data/history/last-fetched.txt'), + 'utf8' + ) .then((res) => JSON.parse(res)) - .catch(() => ({ - ...FILES.reduce((a, b) => { + .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'), + path.join(__dirname, '../data/history/last-fetched.txt'), JSON.stringify({ ...state, [file]: time, @@ -37,6 +40,20 @@ async function updateLastFetched(file, time) { ) } +async function getCounts() { + return fs.promises + .readFile(path.join(__dirname, '../data/history/counts.txt'), 'utf8') + .then((res) => JSON.parse(res)) + .catch(() => ({})) +} + +async function setCounts(obj) { + return fs.promises.writeFile( + path.join(__dirname, '../data/history/counts.txt'), + JSON.stringify(obj) + ) +} + async function getFileContent(file, commit) { return fetch( `https://raw.githubusercontent.com/telegramdesktop/tdesktop/${commit}/${file}` @@ -104,149 +121,23 @@ async function parseRemoteTl(file, commit) { return { layer, content, - tl: await convertTlToJson(content, 'api', true), + tl: convertToArrays(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, '') + return date + .toISOString() + .replace(/[\-:]|\.\d\d\d/g, '') + .split('T')[0] } function shortSha(sha) { return sha.substr(0, 7) } -async function fetchHistory(file, since, defaultParent = null) { +async function fetchHistory(file, since, counts, defaultPrev = null, defaultPrevFile = null) { const history = await (async function () { const ret = [] let page = 1 @@ -278,15 +169,23 @@ async function fetchHistory(file, since, defaultParent = null) { commit.commit.committer.date )}-${shortSha(commit.sha)}.json` + const uid = (schema, commit) => `${schema.layer}_${shortSha(commit.sha)}` + function writeSchemaToFile(schema, commit) { return fs.promises.writeFile( path.join(__dirname, `../data/history/${filename(schema, commit)}`), JSON.stringify({ + // layer is ever-incrementing, sha is random, so no collisions + uid: uid(schema, commit), tl: JSON.stringify(schema.tl), layer: parseInt(schema.layer), + rev: + schema.layer in counts + ? ++counts[schema.layer] + : (counts[schema.layer] = 0), content: schema.content, - // idk where parent: '00' comes from but whatever - parent: schema.parent && schema.parent !== '00' ? schema.parent : defaultParent, + prev: schema.prev ? schema.prev : defaultPrev, + prevFile: schema.prevFile ? schema.prevFile : defaultPrevFile, source: { file, date: commit.commit.committer.date, @@ -315,27 +214,12 @@ async function fetchHistory(file, since, defaultParent = null) { 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() + nextSchema.prev = uid(baseSchema, base) + nextSchema.prevFile = baseFilename() base = next baseSchema = nextSchema await updateLastFetched(file, base.commit.committer.date) + await setCounts(counts) await writeSchemaToFile(baseSchema, base) console.log( 'Fetched commit %s, file %s (%s)', @@ -346,20 +230,26 @@ async function fetchHistory(file, since, defaultParent = null) { } if (file !== CURRENT_FILE) { - await updateLastFetched(file, 'DONE:' + baseFilename()) + await updateLastFetched(file, `DONE:${uid(baseSchema, base)}:${baseFilename()}`) } console.log('No more commits for %s', file) } async function main() { - const last = await getLastFetched() + let last = await getLastFetched() + const counts = await getCounts() + 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) + let parentFile = prev ? last[prev].split(':')[2] : null + + await fetchHistory(file, last[file], counts, parent, parentFile) + + last = await getLastFetched() } } @@ -371,15 +261,16 @@ async function main() { 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' - ) + if (json.prev) { + const parentPath = path.join( + __dirname, + '../data/history', + json.prevFile ) - parentJson.next = parentPath + const parentJson = JSON.parse( + await fs.promises.readFile(parentPath, 'utf-8') + ) + parentJson.next = json.uid await fs.promises.writeFile(parentPath, JSON.stringify(parentJson)) } } diff --git a/packages/tl-reference/scripts/fetch-older-layers.js b/packages/tl-reference/scripts/fetch-older-layers.js new file mode 100644 index 00000000..40d4f0a8 --- /dev/null +++ b/packages/tl-reference/scripts/fetch-older-layers.js @@ -0,0 +1,112 @@ +const { + convertTlToJson, + convertJsonToTl, +} = require('../../tl/scripts/generate-schema') +const fetch = require('node-fetch') +const fs = require('fs') +const path = require('path') +const cheerio = require('cheerio') +const { convertToArrays } = require('./prepare-data') + +const FETCH_UP_TO = 13 + +async function fetchAvailableLayers() { + return fetch('https://core.telegram.org/schema') + .then((i) => i.text()) + .then((html) => { + const $ = cheerio.load(html) + + const links = $('a[href^="?layer="]').toArray().map((it) => it.attribs.href) + + let ret = [] + links.forEach((link) => { + let m = link.match(/\?layer=(\d+)/) + if (m) { + let layer = parseInt(m[1]) + if (layer === 1 || layer > FETCH_UP_TO) return + + ret.push(layer) + } + }) + + return ret + }) +} + +async function fetchFromLayer(layer) { + const html = await fetch('https://core.telegram.org/schema', { + headers: { + cookie: `stel_dev_layer=${layer}`, + }, + }).then((i) => i.text()) + + const $ = cheerio.load(html) + return $('.page_scheme code').text() + .replace(/>/g, '>') + .replace(/</g, '<') + .replace(/&/g, '&') +} + +async function main() { + // find first non-"old" layer, for linking + let firstNext + for (const file of fs.readdirSync( + path.join(__dirname, '../data/history') + )) { + if (file.startsWith(`layer${FETCH_UP_TO + 1}-`)) { + const json = JSON.parse( + fs.readFileSync( + path.join(__dirname, `../data/history/${file}`), + 'utf-8' + ) + ) + firstNext = json.uid + + json.prev = `${FETCH_UP_TO}_FROM_WEBSITE` + json.prevFile = `layer${FETCH_UP_TO}-19700101-0000000.json` + fs.writeFileSync( + path.join(__dirname, `../data/history/${file}`), + JSON.stringify(json) + ) + + break + } + } + + const layers = await fetchAvailableLayers() + + for (const l of layers) { + const tl = await fetchFromLayer(l) + const data = convertTlToJson(tl, 'api', true) + + await fs.promises.writeFile( + path.join( + __dirname, + `../data/history/layer${l}-19700101-0000000.json` + ), + JSON.stringify({ + // layer is ever-incrementing, sha is random, so no collisions + uid: `${l}_FROM_WEBSITE`, + tl: JSON.stringify(convertToArrays(data)), + layer: l, + rev: 0, + content: tl, + prev: l === 2 ? null : `${l - 1}_FROM_WEBSITE`, + prevFile: + l === 2 ? null : `layer${l - 1}-19700101-0000000.json`, + next: l === FETCH_UP_TO ? firstNext : `${l + 1}_FROM_WEBSITE`, + source: { + website: true, + file: '', + date: '1970-01-01T00:00:00Z', + commit: '', + message: '', + }, + }) + ) + + console.log(`Fetched layer ${l}`) + } +} + +main().catch(console.error) diff --git a/packages/tl-reference/scripts/generate-diffs.js b/packages/tl-reference/scripts/generate-diffs.js new file mode 100644 index 00000000..6216ce13 --- /dev/null +++ b/packages/tl-reference/scripts/generate-diffs.js @@ -0,0 +1,59 @@ +const fs = require('fs') +const path = require('path') +const { createTlSchemaDifference } = require('./diff-utils') + +function generateDiffs() { + // first, load all schemas in memory (expensive, but who cares) + const schemas = [] + + for (const file of fs.readdirSync( + path.join(__dirname, '../data/history') + )) { + if (!file.startsWith('layer')) continue + + const fullPath = path.join(__dirname, '../data/history', file) + const json = JSON.parse(fs.readFileSync(fullPath, 'utf-8')) + + json.tl = JSON.parse(json.tl) + delete json.content // useless here + schemas.push(json) + } + + schemas.sort((a, b) => { + if (a.layer !== b.layer) return b.layer - a.layer + + return a.source.date < b.source.date ? 1 : -1 + }) + + // create diff between consecutive pairs. + // that way, we can diff any two given schemas by simply + // merging the diff using `seq` + + let prev = schemas.pop() + let seq = 0 + + while (schemas.length) { + const current = schemas.pop() + + const uid = `${prev.layer}r${prev.rev}-${current.layer}r${current.rev}` + const diff = createTlSchemaDifference(prev, current) + + fs.writeFileSync(path.join(__dirname, `../data/diffs/${uid}.json`), JSON.stringify({ + seq: seq++, + uid, + diff: JSON.stringify(diff), + prev: { + layer: prev.layer, + rev: prev.rev + }, + new: { + layer: current.layer, + rev: current.rev + } + })) + + prev = current + } +} + +generateDiffs() diff --git a/packages/tl-reference/scripts/generate-type-history.js b/packages/tl-reference/scripts/generate-type-history.js new file mode 100644 index 00000000..87b43071 --- /dev/null +++ b/packages/tl-reference/scripts/generate-type-history.js @@ -0,0 +1,151 @@ +const fs = require('fs') +const path = require('path') +const { + createTlSchemaIndex, + createTlUnionsDifference, + createTlConstructorDifference, +} = require('./diff-utils') + +function generateTypeHistory() { + // first, load all schemas in memory (expensive, but who cares) + const schemas = [] + + for (const file of fs.readdirSync( + path.join(__dirname, '../data/history') + )) { + if (!file.startsWith('layer')) continue + + const fullPath = path.join(__dirname, '../data/history', file) + const json = JSON.parse(fs.readFileSync(fullPath, 'utf-8')) + + json.tl = JSON.parse(json.tl) + delete json.content // useless here + schemas.push(json) + } + + // create a set of all types that have ever existed + const types = new Set() + + for (const s of schemas) { + s.tl.classes.forEach((it) => types.add('c_' + it.name)) + s.tl.methods.forEach((it) => types.add('m_' + it.name)) + s.tl.unions.forEach((it) => types.add('u_' + it.type)) + } + + function getSchemaInfo(schema) { + return { + ...schema.source, + layer: schema.layer, + rev: schema.rev + } + } + + schemas.sort((a, b) => { + if (a.layer !== b.layer) return b.layer - a.layer + + return a.source.date < b.source.date ? 1 : -1 + }) + + const history = {} + + const base = schemas.pop() + const baseSchemaInfo = getSchemaInfo(base) + + let prevIndex = createTlSchemaIndex(base.tl) + + Object.entries(prevIndex).forEach(([uid, item]) => { + if (!(history[uid])) history[uid] = [] + + // type was in the first scheme, assume it was added there + history[uid].push({ + action: 'added', + in: baseSchemaInfo, + diff: item + }) + }) + + // for every schema, check changes for each type + + while (schemas.length) { + const schema = schemas.pop() + const schemaInfo = getSchemaInfo(schema) + const newIndex = createTlSchemaIndex(schema.tl) + + types.forEach((uid) => { + if (!(uid in history)) history[uid] = [] + + if (!(uid in prevIndex) && uid in newIndex) { + // type added + history[uid].push({ + action: 'added', + in: schemaInfo, + diff: newIndex[uid] + }) + } + + if (uid in prevIndex && !(uid in newIndex)) { + // type removed + history[uid].push({ + action: 'removed', + in: schemaInfo, + }) + } + + if (uid in prevIndex && uid in newIndex) { + // modified (maybe) + + let diff + if (uid.match(/^u_/)) { + // union + diff = createTlUnionsDifference( + prevIndex[uid], + newIndex[uid] + ) + } else { + diff = createTlConstructorDifference( + prevIndex[uid], + newIndex[uid] + ) + } + + if (diff) { + history[uid].push({ + action: 'modified', + in: schemaInfo, + diff, + }) + } + } + }) + + prevIndex = newIndex + } + + Object.entries(history).forEach(([uid, history]) => { + if (!history.length) return + + history.forEach((it) => { + // for simpler graphql queries + if (it.diff) it.diff = JSON.stringify(it.diff) + }) + + // anti-chronological order + history.reverse() + + fs.writeFileSync( + path.join(__dirname, `../data/types/${uid}.json`), + JSON.stringify({ + uid, + type: { + c: 'class', + m: 'method', + u: 'union' + }[uid[0]], + name: uid.slice(2), + history, + }) + ) + }) +} + +generateTypeHistory() diff --git a/packages/tl-reference/src/components/objects/link-to-tl.tsx b/packages/tl-reference/src/components/objects/link-to-tl.tsx index 3a8fb4c1..771a39e9 100644 --- a/packages/tl-reference/src/components/objects/link-to-tl.tsx +++ b/packages/tl-reference/src/components/objects/link-to-tl.tsx @@ -4,26 +4,29 @@ import React from 'react' import { ExtendedTlObject } from '../../types' -export function LinkToTl(name: string): React.ReactElement -export function LinkToTl(obj: ExtendedTlObject): React.ReactElement +export function LinkToTl(name: string, history?: boolean): React.ReactElement +export function LinkToTl(obj: ExtendedTlObject, history?: boolean): React.ReactElement export function LinkToTl( prefix: string, type: string, - name: string + name: string, + history?: boolean ): React.ReactElement export function LinkToTl( prefix: string | ExtendedTlObject, - type?: string, - name?: string + type?: string | boolean, + name?: string, + history?: boolean ): React.ReactElement { if (typeof prefix !== 'string') { type = prefix.type name = prefix.name prefix = prefix.prefix + history = !!type } // this kind of invocation is used in parameters table and for return type - if (!type && !name) { + if ((!type || typeof type === 'boolean') && !name) { const fullType = prefix // core types @@ -53,11 +56,14 @@ export function LinkToTl( } // must be union since this is from parameters type + history = !!type prefix = '' type = 'union' name = fullType } + if (history) type = 'history/' + type + return ( {prefix} diff --git a/packages/tl-reference/src/components/objects/object-parameters.tsx b/packages/tl-reference/src/components/objects/object-parameters.tsx new file mode 100644 index 00000000..f0678cc2 --- /dev/null +++ b/packages/tl-reference/src/components/objects/object-parameters.tsx @@ -0,0 +1,118 @@ +import { ExtendedTlObject } from '../../types' +import { + createStyles, + makeStyles, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from '@material-ui/core' +import { LinkToTl } from './link-to-tl' +import { Description } from '../page' +import React from 'react' + +import { green, red, blue } from '@material-ui/core/colors' +import clsx from 'clsx' + +const useStyles = makeStyles((theme) => + createStyles({ + table: { + '& th, & td': { + fontSize: 15, + }, + }, + mono: { + fontFamily: 'Fira Mono, Consolas, monospace', + }, + bold: { + fontWeight: 'bold', + }, + changed: { + fontWeight: 500, + border: 'none', + width: 100, + }, + added: { + backgroundColor: + theme.palette.type === 'light' ? green[100] : green[900], + color: theme.palette.type === 'light' ? green[900] : green[100], + }, + modified: { + backgroundColor: + theme.palette.type === 'light' ? blue[100] : blue[900], + color: theme.palette.type === 'light' ? blue[900] : blue[100], + }, + removed: { + backgroundColor: + theme.palette.type === 'light' ? red[100] : red[900], + color: theme.palette.type === 'light' ? red[900] : red[100], + }, + }) +) + +export function ObjectParameters({ + obj, + diff, + history, +}: { + obj: ExtendedTlObject + diff?: boolean + history?: boolean +}): JSX.Element { + const classes = useStyles() + + return ( + + + + {diff && Change} + Name + Type + Description + + + + {obj.arguments.map((arg) => ( + + {diff && ( + + {arg.changed} + + )} + + + {arg.name} + + + + {arg.optional ? ( + + {LinkToTl(arg.type, history)}? + + ) : ( + LinkToTl(arg.type, history) + )} + + + + ))} + +
+ ) +} diff --git a/packages/tl-reference/src/components/objects/object-ts-code.tsx b/packages/tl-reference/src/components/objects/object-ts-code.tsx new file mode 100644 index 00000000..8d26f457 --- /dev/null +++ b/packages/tl-reference/src/components/objects/object-ts-code.tsx @@ -0,0 +1,138 @@ +import { ExtendedTlObject } from '../../types' +import React, { ReactNode } from 'react' +import { useCodeArea } from '../../hooks/use-code-area' + +export function ObjectTsCode({ + obj, + children, +}: { + obj: ExtendedTlObject + children?: ExtendedTlObject[] +}): JSX.Element { + const code = useCodeArea() + + const entities: ReactNode[] = [] + if (obj.type === 'union') { + entities.push( + code.keyword('export type'), + ' ', + code.identifier(obj.ts), + ' =' + ) + + children!.forEach((it) => { + const ns = + it.namespace === '$root' + ? it.prefix === 'mtproto/' + ? 'mtproto.' + : '' + : it.namespace + '.' + + entities.push('\n | ', code.typeName(`tl.${ns}${it.ts}`)) + }) + } else { + entities.push( + code.keyword('export interface'), + ' ', + code.identifier(obj.ts), + ' {\n ', + code.property('_'), + ': ', + code.string( + `'${obj.prefix === 'mtproto/' ? 'mt_' : ''}${obj.name}'` + ) + ) + + obj.arguments.forEach((arg) => { + if (arg.type === '$FlagsBitField') { + return entities.push( + code.comment( + `\n // ${arg.name}: TlFlags // handled automatically` + ) + ) + } + + entities.push( + '\n ', + code.property(arg.name), + `${arg.optional ? '?' : ''}: `, + code.typeName(arg.ts) + ) + + if (arg.predicate) { + entities.push( + ' ', + code.comment('// present if ' + arg.predicate) + ) + } + }) + + entities.push('\n}') + } + + return code.code(entities) + + // const typeName = (s: string): string => { + // if ( + // s === 'string' || + // s === 'number' || + // s === 'boolean' || + // s === 'true' + // ) { + // return keyword(s) + // } + // + // if (s.substr(s.length - 2) === '[]') + // return typeName(s.substr(0, s.length - 2)) + '[]' + // + // return s.split('.').map(identifier).join('.') + // } + // + // let html + // if (obj.type === 'union') { + // html = `${keyword('export type')} ${identifier(obj.ts)} =` + // html += children! + // .map((it) => { + // const ns = + // it.namespace === '$root' + // ? it.prefix === 'mtproto/' + // ? 'mtproto.' + // : '' + // : it.namespace + '.' + // + // return `\n | ${typeName(`tl.${ns}${it.ts}`)}` + // }) + // .join('') + // } else { + // html = `${keyword('export interface')} ${identifier(obj.ts)} {` + // html += `\n ${property('_')}: ` + // html += _string( + // `'${obj.prefix === 'mtproto/' ? 'mt_' : ''}${obj.name}'` + // ) + // html += obj.arguments + // .map((arg) => { + // if (arg.type === '$FlagsBitField') { + // return comment( + // `\n // ${arg.name}: TlFlags // handled automatically` + // ) + // } + // + // const opt = arg.optional ? '?' : '' + // const comm = arg.predicate + // ? ' ' + comment('// present if ' + arg.predicate) + // : '' + // + // const typ = typeName(arg.ts) + // return `\n ${property(arg.name)}${opt}: ${typ}${comm}` + // }) + // .join('') + // html += '\n}' + // } + // + // return ( + //
+    // )
+}
diff --git a/packages/tl-reference/src/components/page.tsx b/packages/tl-reference/src/components/page.tsx
index 05d2548a..b06043eb 100644
--- a/packages/tl-reference/src/components/page.tsx
+++ b/packages/tl-reference/src/components/page.tsx
@@ -51,6 +51,11 @@ export const usePageStyles = makeStyles((theme) =>
         paragraph: {
             marginBottom: theme.spacing(2),
         },
+        rev: {
+            fontSize: 16,
+            fontWeight: 500,
+            marginLeft: 2,
+        },
     })
 )
 
@@ -80,7 +85,8 @@ export function Page({
                             
                                 open-source
                             {' '}
-                            and licensed under MIT.
+ and licensed under MIT. +
This website is not affiliated with Telegram. @@ -92,7 +98,7 @@ export function Page({ } export function Description(params: { - description: string | null + description?: string | null component?: any className?: string }) { @@ -135,6 +141,33 @@ export function ListItemTlObject({ node }: { node: ExtendedTlObject }) { ) } +export function ListItemTlLink({ + name, + type, + history, +}: { + type: string + name: string + history?: boolean +}) { + return ( + <> +
+ + + {name} + + + +
+ + + ) +} + export function Section({ title, id, diff --git a/packages/tl-reference/src/components/table-of-contents.tsx b/packages/tl-reference/src/components/table-of-contents.tsx index d78d1c21..a19f5332 100644 --- a/packages/tl-reference/src/components/table-of-contents.tsx +++ b/packages/tl-reference/src/components/table-of-contents.tsx @@ -9,14 +9,14 @@ import clsx from 'clsx' const useStyles = makeStyles((theme) => createStyles({ root: { - top: 80, + top: 0, // Fix IE 11 position sticky issue. width: 175, flexShrink: 0, order: 2, position: 'sticky', height: 'calc(100vh - 80px)', - overflowY: 'auto', + overflowX: 'auto', padding: theme.spacing(2, 2, 2, 0), display: 'none', [theme.breakpoints.up('sm')]: { @@ -37,6 +37,9 @@ const useStyles = makeStyles((theme) => padding: theme.spacing(0.5, 0, 0.5, 1), borderLeft: '4px solid transparent', boxSizing: 'content-box', + overflow: 'hidden', + textOverflow: 'ellipsis', + '&:hover': { borderLeft: `4px solid ${ theme.palette.type === 'light' diff --git a/packages/tl-reference/src/components/tl-schema-code.tsx b/packages/tl-reference/src/components/tl-schema-code.tsx new file mode 100644 index 00000000..434b1d2a --- /dev/null +++ b/packages/tl-reference/src/components/tl-schema-code.tsx @@ -0,0 +1,122 @@ +import { useCodeArea } from '../hooks/use-code-area' +import { ReactNode } from 'react' +import { Link } from 'gatsby' +import React from 'react' + +const LineRegex = /^(.+?)(?:#([0-f]{1,8}))?(?: \?)?(?: {(.+?:.+?)})? ((?:.+? )*)= (.+);$/ + +export function TlSchemaCode({ tl }: { tl: string }) { + const code = useCodeArea() + + const highlightType = (s: string): ReactNode[] => { + if ( + s === '#' || + s === 'int' || + s === 'long' || + s === 'double' || + s === 'string' || + s === 'bytes' + ) + return [code.keyword(s)] + if (s.match(/^[Vv]ector<(.+?)>$/)) { + return [ + code.identifier(s.substr(0, 6)), + '<', + ...highlightType(s.substring(7, s.length - 1)), + '>', + ] + } + + return [{code.identifier(s)}] + } + + let inTypes = true + const entities: ReactNode[] = [] + + tl.split('\n').forEach((line) => { + if (line.match(/^\/\//)) { + return entities.push(code.comment(line + '\n')) + } + + let m + if ((m = line.match(LineRegex))) { + const [, fullName, typeId, generics, args, type] = m + + entities.push( + + {code.identifier(fullName)} + + ) + if (typeId) { + entities.push('#', code.string(typeId)) + } + + if (generics) { + entities.push(' {') + generics.split(' ').forEach((pair) => { + const [name, type] = pair.trim().split(':') + entities.push( + code.property(name), + ':', + code.identifier(type) + ) + }) + entities.push('}') + } + + if (args) { + if (args.trim().match(/\[ [a-z]+ ]/i)) { + // for generics + entities.push(' ', code.comment(args.trim())) + } else { + const parsed = args + .trim() + .split(' ') + .map((j) => j.split(':')) + + if (parsed.length) { + parsed.forEach(([name, typ]) => { + const [predicate, type] = typ.split('?') + + if (!type) { + return entities.push( + ' ', + code.property(name), + ':', + ...highlightType(predicate) + ) + } + + return entities.push( + ' ', + code.property(name), + ':', + code.string(predicate), + '?', + ...highlightType(type) + ) + }) + } + } + } + + entities.push( + ' = ', + {code.identifier(type)} + ) + + entities.push(';\n') + return + } + + if (line.match(/^---(functions|types)---$/)) { + inTypes = line === '---types---' + return entities.push(code.keyword(line + '\n')) + } + + // unable to highlight + return entities.push(line + '\n') + }) + + return code.code(entities) +} diff --git a/packages/tl-reference/src/hooks/use-code-area.tsx b/packages/tl-reference/src/hooks/use-code-area.tsx new file mode 100644 index 00000000..d58da448 --- /dev/null +++ b/packages/tl-reference/src/hooks/use-code-area.tsx @@ -0,0 +1,93 @@ +import { createStyles, makeStyles } from '@material-ui/core' +import React, { ReactNode } from 'react' + +const useStyles = makeStyles((theme) => + createStyles({ + // theme ported from one dark + code: { + fontFamily: 'Iosevka SS05, Fira Mono, Consolas, monospace', + background: '#282c34', + color: '#bbbbbb', + fontSize: 16, + borderRadius: 4, + overflowX: 'auto', + padding: 8, + + '& a': { + textDecoration: 'none' + } + }, + keyword: { + fontStyle: 'italic', + color: '#c678dd', + }, + identifier: { + color: '#e5c07b', + }, + property: { + color: '#e06c75', + }, + comment: { + color: '#5c6370', + }, + string: { + color: '#98c379', + }, + }) +) + +export function useCodeArea() { + const classes = useStyles() + + const keyword = (s: ReactNode) => ( + {s} + ) + + const identifier = (s: ReactNode) => ( + {s} + ) + + const property = (s: ReactNode) => ( + {s} + ) + + const comment = (s: ReactNode) => ( + {s} + ) + + const string = (s: ReactNode) => {s} + + const typeName = (s: string): ReactNode => { + if ( + s === 'string' || + s === 'number' || + s === 'boolean' || + s === 'any' || + s === 'true' + ) { + return keyword(s) + } + + if (s.substr(s.length - 2) === '[]') + return [typeName(s.substr(0, s.length - 2)), '[]'] + + const ret: ReactNode[] = [] + s.split('.').forEach((it, idx) => { + if (idx !== 0) ret.push('.') + ret.push(identifier(it)) + }) + return ret + } + + const code = (s: ReactNode) =>
{s}
+ + return { + keyword, + identifier, + property, + comment, + string, + typeName, + code, + } +} diff --git a/packages/tl-reference/src/layout.tsx b/packages/tl-reference/src/layout.tsx index 920da971..bc932262 100644 --- a/packages/tl-reference/src/layout.tsx +++ b/packages/tl-reference/src/layout.tsx @@ -42,11 +42,11 @@ const pages = [ name: 'Methods', regex: /^(?:\/tl)?(?:\/mtproto)?\/methods?(\/|$)/, }, - // { - // path: '/history', - // name: 'History', - // regex: /^\/history(\/|$)/, - // }, + { + path: '/history', + name: 'History', + regex: /^\/history(\/|$)/, + }, ] const drawerWidth = 240 diff --git a/packages/tl-reference/src/pages/404.tsx b/packages/tl-reference/src/pages/404.tsx index a8205f6c..9d15b0c6 100644 --- a/packages/tl-reference/src/pages/404.tsx +++ b/packages/tl-reference/src/pages/404.tsx @@ -1,10 +1,25 @@ import * as React from 'react' -import { Typography } from '@material-ui/core' +import { Typography, Link as MuiLink } from '@material-ui/core' import { Page, usePageStyles } from '../components/page' import { Helmet } from 'react-helmet' +import { Link } from 'gatsby' -const NotFoundPage = () => { +const NotFoundPage = ({ location }: any) => { const classes = usePageStyles() + const path: string = location.pathname + + let historyReference = undefined + let m + if ((m = path.match(/^(?:\/tl|\/)((?:class|union|method)\/.+?)$/))) { + historyReference = ( + + This type might no longer exist, but you could check{' '} + + History section + + + ) + } return ( @@ -20,6 +35,7 @@ const NotFoundPage = () => { This page does not exist + {historyReference} ) } diff --git a/packages/tl-reference/src/pages/history.tsx b/packages/tl-reference/src/pages/history.tsx index ac57979c..f06bb91c 100644 --- a/packages/tl-reference/src/pages/history.tsx +++ b/packages/tl-reference/src/pages/history.tsx @@ -1,20 +1,100 @@ import React from 'react' -import { Page, usePageStyles } from '../components/page' -import { Typography } from '@material-ui/core' +import { Page, Section, usePageStyles } from '../components/page' +import { Link as MuiLink, Typography } from '@material-ui/core' +import { Helmet } from 'react-helmet' +import { graphql, Link } from 'gatsby' -export default function HistoryPage() { +interface GraphqlResult { + layers: { + nodes: { + layer: number + rev: number + source: { + date: string + commit: string + website: boolean + file: string + } + }[] + } +} + +export default function HistoryPage({ data }: { data: GraphqlResult }) { const classes = usePageStyles() + data.layers.nodes.sort((a, b) => + a.layer === b.layer ? b.rev - a.rev : b.layer - a.layer + ) + return ( + + History + +
History
- This page is currently under construction + In this section of the website, you can explore history of the + TL schema, and how it changed over the time. +
+
+ Schemas are fetched automatically from + tdesktop + {' '} + repository, and older schemas (<14) are fetched directly from + Telegram's website.
+ +
+ {data.layers.nodes.map((layer) => ( + + + Layer {layer.layer} + {layer.rev > 0 && ( + + {' '} + rev. {layer.rev} + + )} + + + + {' '} + (from{' '} + {layer.source.website + ? 'website' + : layer.source.date} + ) + + + ))} +
) } + +export const query = graphql` + query { + layers: allHistoryJson { + nodes { + layer + rev + source { + website + date(formatString: "DD-MM-YYYY") + commit + file + } + } + } + } +` diff --git a/packages/tl-reference/src/pages/index.tsx b/packages/tl-reference/src/pages/index.tsx index b822a270..5ac30b73 100644 --- a/packages/tl-reference/src/pages/index.tsx +++ b/packages/tl-reference/src/pages/index.tsx @@ -26,6 +26,7 @@ interface Data { nodes: [ { layer: number + rev: number source: { date: string commit: string @@ -34,6 +35,9 @@ interface Data { } ] } + + historySchemas: { totalCount: number } + historyTypes: { totalCount: number } } function countMissingDescriptionArguments( @@ -56,6 +60,8 @@ export default function IndexPage({ data }: { data: Data }) { countMissingDescriptionArguments(data.argWithoutDesc, true) countMissingDescriptionArguments(data.argWithDesc, false) + const currentLayer = data.updated.nodes[0] + return ( - layer {data.updated.nodes[0].layer} / updated{' '} - {data.updated.nodes[0].source.date} + layer {currentLayer.layer} + {currentLayer.rev > 0 ? ` rev. ${currentLayer.rev}` : ''} / + updated {currentLayer.source.date} /{' '} + + view source + @@ -284,6 +299,13 @@ export default function IndexPage({ data }: { data: Data }) { ) })()} +
  • + History is available for{' '} + + {data.historySchemas.totalCount} schemas + {' '} + and {data.historyTypes.totalCount} types +
  • ) @@ -329,6 +351,7 @@ export const query = graphql` ) { nodes { layer + rev source { date(formatString: "DD-MM-YYYY") commit @@ -363,5 +386,13 @@ export const query = graphql` } } } + + historySchemas: allHistoryJson { + totalCount + } + + historyTypes: allTypesJson { + totalCount + } } ` diff --git a/packages/tl-reference/src/pages/no-description.tsx b/packages/tl-reference/src/pages/no-description.tsx index 9b5a5083..720aa7e3 100644 --- a/packages/tl-reference/src/pages/no-description.tsx +++ b/packages/tl-reference/src/pages/no-description.tsx @@ -85,7 +85,11 @@ export default function NoDescriptionPage({ data }: { data: Data }) { {LinkToTl(node)} - {(node.type === 'method' ? 'm_' : 'o_') + + {(node.type === 'method' + ? 'm_' + : node.type === 'union' + ? 'u_' + : 'o_') + (node.prefix === 'mtproto/' ? 'mt_' : '') + diff --git a/packages/tl-reference/src/templates/tl-layer.tsx b/packages/tl-reference/src/templates/tl-layer.tsx new file mode 100644 index 00000000..e7193210 --- /dev/null +++ b/packages/tl-reference/src/templates/tl-layer.tsx @@ -0,0 +1,273 @@ +import React, { ReactNode, useState } from 'react' +import { graphql, Link } from 'gatsby' +import { Page, usePageStyles } from '../components/page' +import { + Breadcrumbs, + Button, + createStyles, + Link as MuiLink, + makeStyles, + Snackbar, + Typography, +} from '@material-ui/core' +import { Spacer } from '../components/spacer' +import { TlSchemaCode } from '../components/tl-schema-code' +import { Helmet } from 'react-helmet' + +import ChevronLeftIcon from '@material-ui/icons/ChevronLeft' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' +import CodeIcon from '@material-ui/icons/Code' +import CloudDownloadIcon from '@material-ui/icons/CloudDownload' + +interface GraphqlResult { + layer: { + layer: number + rev: number + content: string + source: { + date: string + commit: string + website: boolean + file: string + } + } + + prev: { + layer: number + rev: number + } + + next: { + layer: number + rev: number + } +} + +const useStyles = makeStyles((theme) => + createStyles({ + navigation: { + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + width: '100%', + }, + btn: { + margin: theme.spacing(1), + }, + }) +) + +export default function TlLayer({ + data: { layer, prev, next }, +}: { + data: GraphqlResult +}) { + const pageClasses = usePageStyles() + const classes = useStyles() + + const [snackText, setSnackText] = useState(undefined) + + function copyToClipboard() { + // https://stackoverflow.com/a/30810322 + const area = document.createElement('textarea') + area.style.position = 'fixed' + area.style.top = '0' + area.style.left = '0' + area.style.width = '2em' + area.style.height = '2em' + area.style.padding = '0' + area.style.border = 'none' + area.style.outline = 'none' + area.style.boxShadow = 'none' + area.style.background = 'transparent' + + area.value = layer.content + + document.body.appendChild(area) + area.focus() + area.select() + + document.execCommand('copy') + document.body.removeChild(area) + + setSnackText('Copied to clipboard!') + } + + function downloadAsFile() { + const link = document.createElement('a') + link.setAttribute( + 'href', + 'data:text/plain;charset=utf-8,' + encodeURIComponent(layer.content) + ) + link.setAttribute( + 'download', + `layer${layer.layer}${layer.rev ? `-rev${layer.rev}` : ''}.tl` + ) + + link.style.display = 'none' + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + } + + return ( + + + + {`Layer ${layer.layer}` + + `${layer.rev > 0 ? ` rev. ${layer.rev}` : ''}`} + + 0 && ` rev. ${layer.rev}`}` + + ` (from ${ + layer.source.website ? 'website' : layer.source.date + })` + } + /> + + +
    + {prev && ( + + )} + + {next && ( + + )} +
    + +
    + + + History + + + Layer {layer.layer} + {layer.rev > 0 && ` rev. ${layer.rev}`} + + + + Layer {layer.layer} + {layer.rev > 0 && ( + + {' '} + rev. {layer.rev} + + )} + + + from {layer.source.website ? 'website' : layer.source.date} + {!layer.source.website && ( + <> + {' '} + / commit{' '} + + {layer.source.commit.substr(0, 7)} + {' '} + ( + + file + + ) + + )} + +
    + + setSnackText(undefined)} + message={snackText} + /> +
    + + +
    + + +
    + ) +} + +export const query = graphql` + query($layer: Int!, $rev: Int!, $prev: String, $next: String) { + layer: historyJson(layer: { eq: $layer }, rev: { eq: $rev }) { + layer + rev + content + prev + next + source { + website + date(formatString: "DD-MM-YYYY") + commit + file + } + } + + prev: historyJson(uid: { eq: $prev }) { + layer + rev + } + + next: historyJson(uid: { eq: $next }) { + layer + rev + } + } +` diff --git a/packages/tl-reference/src/templates/tl-object.tsx b/packages/tl-reference/src/templates/tl-object.tsx index 2a659964..b1b79d7b 100644 --- a/packages/tl-reference/src/templates/tl-object.tsx +++ b/packages/tl-reference/src/templates/tl-object.tsx @@ -26,6 +26,8 @@ import { Link } from 'gatsby' import { LinkToTl } from '../components/objects/link-to-tl' import { TableOfContentsItem } from '../components/table-of-contents' import { Helmet } from 'react-helmet' +import { ObjectParameters } from '../components/objects/object-parameters' +import { ObjectTsCode } from '../components/objects/object-ts-code' interface GraphqlResult { self: ExtendedTlObject @@ -46,38 +48,6 @@ const useStyles = makeStyles((theme) => fontSize: 15, }, }, - mono: { - fontFamily: 'Fira Mono, Consolas, monospace', - }, - // theme ported from one dark - code: { - fontFamily: 'Fira Mono, Consolas, monospace', - background: '#282c34', - color: '#bbbbbb', - fontSize: 16, - borderRadius: 4, - overflowX: 'auto', - padding: 8, - }, - keyword: { - fontStyle: 'italic', - color: '#c678dd', - }, - identifier: { - color: '#e5c07b', - }, - property: { - color: '#e06c75', - }, - comment: { - color: '#5c6370', - }, - string: { - color: '#98c379', - }, - bold: { - fontWeight: 'bold', - }, }) ) @@ -109,41 +79,6 @@ export default function TlObject({ data }: { data: GraphqlResult }) { const obj = data.self const toc = useToc(obj) - const keyword = (s: string) => - `${s}` - const identifier = (s: string) => - `${s}` - const property = (s: string) => - `${s}` - const comment = (s: string) => - `${s}` - const _string = (s: string) => `${s}` - - const typeName = (s: string): string => { - if ( - s === 'string' || - s === 'number' || - s === 'boolean' || - s === 'true' - ) { - return keyword(s) - } - - if (s.substr(s.length - 2) === '[]') - return typeName(s.substr(0, s.length - 2)) + '[]' - - return s.split('.').map(identifier).join('.') - } - - const code = (s: string) => { - return ( -
    -        )
    -    }
    -
         return (
             
                 
    @@ -222,6 +157,17 @@ export default function TlObject({ data }: { data: GraphqlResult }) {
                                 
                             )
                         )}
    +                    {obj.prefix === '' && (
    +                        <>
    +                            {' / '}
    +                            
    +                                history
    +                            
    +                        
    +                    )}
                     
                 
                 
                 {obj.type !== 'union' && (
                     
    - - - - Name - Type - Description - - - - {obj.arguments.map((arg) => ( - - - - {arg.name} - - - - {LinkToTl(arg.type)} - {arg.optional ? '?' : ''} - - - - ))} - -
    +
    )} {obj.type === 'union' && ( @@ -343,61 +255,9 @@ export default function TlObject({ data }: { data: GraphqlResult }) { )} - - TypeScript declaration - - - {/* this is a mess, but who cares */} - {code( - obj.type === 'union' - ? `${keyword('export type')} ${identifier(obj.ts)} =` + - data.children.nodes - .map( - (it) => - `\n | ${typeName( - 'tl.' + - (it.namespace === '$root' - ? it.prefix === 'mtproto/' - ? 'mtproto.' - : '' - : it.namespace + '.') + - it.ts - )}` - ) - .join('') - : `${keyword('export interface')} ${identifier(obj.ts)} {` + - `\n ${property('_')}: ${_string( - `'${obj.prefix === 'mtproto/' ? 'mt_' : ''}${ - obj.name - }'` - )}` + - obj.arguments - .map((arg) => - arg.type === '$FlagsBitField' - ? comment( - '\n // ' + - arg.name + - ': TlFlags // handled automatically' - ) - : `\n ${property(arg.name)}${ - arg.optional ? '?' : '' - }: ${typeName(arg.ts)}${ - arg.predicate - ? ' ' + - comment( - '// present if ' + - arg.predicate - ) - : '' - }` - ) - .join('') + - '\n}' - )} +
    + +
    ) } diff --git a/packages/tl-reference/src/templates/type-history.tsx b/packages/tl-reference/src/templates/type-history.tsx new file mode 100644 index 00000000..18fcc1f1 --- /dev/null +++ b/packages/tl-reference/src/templates/type-history.tsx @@ -0,0 +1,409 @@ +import { + Description, + ListItemTlLink, + ListItemTlObject, + Page, + Section, + usePageStyles, +} from '../components/page' +import React from 'react' +import { graphql, Link } from 'gatsby' +import { ExtendedTlObject } from '../types' +import { + Breadcrumbs, + createStyles, + Divider, + Link as MuiLink, + List, + makeStyles, + Typography, +} from '@material-ui/core' +import { LinkToTl } from '../components/objects/link-to-tl' +import { TableOfContentsItem } from '../components/table-of-contents' +import { ObjectParameters } from '../components/objects/object-parameters' +import { hexConstructorId } from '../utils' +import { Helmet } from 'react-helmet' + +interface GraphqlResult { + info: { + uid: string + type: string + name: string + history: { + action: 'added' | 'modified' | 'removed' + diff: string + in: { + date: string + layer: number + rev: number + commit: string + website: boolean + file: string + } + }[] + } + object: ExtendedTlObject +} + +const useStyles = makeStyles((theme) => + createStyles({ + description: { + marginBottom: theme.spacing(2), + fontSize: 16, + }, + fakeStrikethrough: { + textDecoration: 'line-through', + '&:hover': { + textDecoration: 'none', + }, + }, + }) +) + +const capitalize = (s: string) => s[0].toUpperCase() + s.substr(1) + +export default function TypeHistoryPage({ + data, + pageContext, +}: { + data: GraphqlResult + pageContext: ExtendedTlObject // in fact not, but who cares +}) { + const pageClasses = usePageStyles() + const classes = useStyles() + + const obj = data.object ?? pageContext + const history = data.info.history + const first = history[history.length - 1] + + const toc: TableOfContentsItem[] = [{ id: 'title', title: obj.name }] + + history.forEach((item) => + toc.push({ + id: `layer${item.in.layer}${ + item.in.rev ? `-rev${item.in.rev}` : '' + }`, + title: `Layer ${item.in.layer}${ + item.in.rev ? ` rev. ${item.in.rev}` : '' + }`, + }) + ) + + // documentation is not fetched for historical schemas (yet?) + const fillDescriptionFromCurrent = (it: ExtendedTlObject): void => { + if (!it.arguments || !obj.arguments) return + + it.arguments.forEach((arg) => { + if (arg.description) return + + const curr = obj.arguments.find((i) => i.name === arg.name) + if (curr) arg.description = curr.description + }) + } + + const HistoryItem = ( + item: GraphqlResult['info']['history'][number] + ): JSX.Element => { + let content: JSX.Element | undefined = undefined + + if (pageContext.type === 'union') { + if (item.action === 'added') { + content = ( + <> + Types + + {JSON.parse(item.diff).subtypes.map( + (type: string) => ( + + ) + )} + + + ) + } else if (item.action === 'modified') { + let added = undefined + let removed = undefined + + const diff = JSON.parse(item.diff).subtypes + + if (diff.added.length) { + added = ( + <> + Added + + {diff.added.map((type: string) => ( + + ))} + + + ) + } + + if (diff.removed.length) { + removed = ( + <> + Removed + + {diff.removed.map((type: string) => ( + + ))} + + + ) + } + + content = ( + <> + {added} + {removed} + + ) + } + } else { + if (item.action === 'added') { + const object = JSON.parse(item.diff) + fillDescriptionFromCurrent(object) + + content = ( + <> + + Constructor ID: {hexConstructorId(object.id)} +
    + {object.returns ? ( + <>Returns: {LinkToTl(object.returns, true)} + ) : ( + <>Belongs to: {LinkToTl(object.type, true)} + )} +
    + Parameters + + + ) + } else if (item.action === 'modified') { + const stub: ExtendedTlObject = { + arguments: [], + } as any + + const diff = JSON.parse(item.diff) + + if (diff.arguments) { + diff.arguments.added.forEach((arg: any) => + stub.arguments.push({ ...arg, changed: 'added' }) + ) + diff.arguments.modified.forEach((arg: any) => { + stub.arguments.push({ + ...arg.old, + changed: 'modified', + className: classes.fakeStrikethrough, + }) + stub.arguments.push({ ...arg.new, changed: 'modified' }) + }) + diff.arguments.removed.forEach((arg: any) => + stub.arguments.push({ ...arg, changed: 'removed' }) + ) + } + fillDescriptionFromCurrent(stub) + + let constructorId = undefined + let returns = undefined + let union = undefined + + if (diff.id) { + constructorId = ( + + Constructor ID:{' '} + + {hexConstructorId(diff.id.old)} + {' '} + → {hexConstructorId(diff.id.new)} + + ) + } + + if (diff.returns) { + returns = ( + + Returns:{' '} + + {LinkToTl(diff.returns.old, true)} + {' '} + → {LinkToTl(diff.returns.new, true)} + + ) + } + + if (diff.type) { + union = ( + + Belongs to:{' '} + + {LinkToTl(diff.type.old, true)} + {' '} + → {LinkToTl(diff.type.new, true)} + + ) + } + + content = ( + <> + + {constructorId} + {returns} + {union} + + Parameters + {diff.arguments && ( + + )} + + ) + } + } + + return ( + <> +
    + + {capitalize(item.action)} in Layer {item.in.layer} + {item.in.rev > 0 && ( + + {' '} + rev. {item.in.rev} + + )} + + + on {item.in.website ? 'website' : item.in.date} + {!item.in.website && ( + <> + {' '} + / commit{' '} + + {item.in.commit.substr(0, 7)} + {' '} + ( + + file + + ) + + )} + +
    + {content} + + ) + } + + return ( + + + History of {obj.name} + + + +
    + + + History + + Types + {obj.name} + + + {obj.name} + + + first introduced in layer {first.in.layer} on{' '} + {first.in.website ? 'website' : first.in.date} + {data.object && ( + <> + {' '} + /{' '} + + current + + + )} + +
    + + {history.map(HistoryItem)} +
    + ) +} + +export const query = graphql` + query($uid: String!, $name: String!, $type: String!) { + info: typesJson(uid: { eq: $uid }) { + uid + type + name + history { + action + diff + in { + date(formatString: "DD-MM-YYYY") + layer + rev + commit + file + website + } + } + } + + object: tlObject( + prefix: { eq: "" } + name: { eq: $name } + type: { eq: $type } + ) { + prefix + type + name + description + arguments { + name + description + } + } + } +` diff --git a/packages/tl-reference/src/types.tsx b/packages/tl-reference/src/types.tsx index b45606bc..6dd7e553 100644 --- a/packages/tl-reference/src/types.tsx +++ b/packages/tl-reference/src/types.tsx @@ -17,6 +17,9 @@ export interface ExtendedTlObject { type: string predicate: string description: string | null + + changed?: 'added' | 'modified' | 'removed' + className?: string }[] throws: { name: string diff --git a/packages/tl-reference/src/utils.ts b/packages/tl-reference/src/utils.ts index 403a5181..340fbfa9 100644 --- a/packages/tl-reference/src/utils.ts +++ b/packages/tl-reference/src/utils.ts @@ -18,3 +18,7 @@ export const isTouchDevice = function (): boolean { const query = prefixes.map(i => `(${i}touch-enabled)`).join(',') return mq(query) } + +export const hexConstructorId = (id: number): string => { + return '0x' + id.toString(16).padStart(8, '0') +} diff --git a/packages/tl/scripts/generate-schema.js b/packages/tl/scripts/generate-schema.js index 089bb30b..35d3886f 100644 --- a/packages/tl/scripts/generate-schema.js +++ b/packages/tl/scripts/generate-schema.js @@ -84,7 +84,7 @@ function getJSType(typ, argName) { return normalizeGenerics(typ) } -async function convertTlToJson(tlText, tlType, silent = false) { +function convertTlToJson(tlText, tlType, silent = false) { let lines = tlText.split('\n') let pos = 0 let line = lines[0].trim() @@ -470,9 +470,9 @@ function convertJsonToTl(json) { json.methods = json.methods.filter((it) => it.method !== 'http_wait') json.constructors.push(httpWait) - json.constructors.forEach(objectToLine) + json.constructors.filter(Boolean).forEach(objectToLine) lines.push('---functions---') - json.methods.forEach(objectToLine) + json.methods.filter(Boolean).forEach(objectToLine) return lines.join('\n') } @@ -494,14 +494,14 @@ async function main() { .then((json) => convertJsonToTl(json)) let ret = {} - ret.mtproto = await convertTlToJson(mtprotoTl, 'mtproto') + ret.mtproto = convertTlToJson(mtprotoTl, 'mtproto') console.log('[i] Fetching api.tl') let apiTl = await fetch( 'https://raw.githubusercontent.com/telegramdesktop/tdesktop/dev/Telegram/Resources/tl/api.tl' ).then((i) => i.text()) ret.apiLayer = apiTl.match(/^\/\/ LAYER (\d+)/m)[1] - ret.api = await convertTlToJson(apiTl, 'api') + ret.api = convertTlToJson(apiTl, 'api') await addDocumentation(ret.api) await applyDescriptionsFile(ret, descriptionsYaml) @@ -526,7 +526,8 @@ async function main() { } module.exports = { - convertTlToJson + convertTlToJson, + convertJsonToTl } if (require.main === module) {