feat(tl-ref): history section, containing type history and older schemas

This commit is contained in:
teidesu 2021-06-05 17:46:58 +03:00
parent 685d75effd
commit 599250d0af
27 changed files with 1996 additions and 362 deletions

View file

@ -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`

View file

@ -1 +1,2 @@
*.json
*.txt

View file

@ -0,0 +1 @@
*.json

View file

@ -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',
},
})
})
})

View file

@ -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,
}

View file

@ -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)
if (json.prev) {
const parentPath = path.join(
__dirname,
'../data/history',
json.prevFile
)
const parentJson = JSON.parse(
await fs.promises.readFile(
parentPath,
'utf-8'
await fs.promises.readFile(parentPath, 'utf-8')
)
)
parentJson.next = parentPath
parentJson.next = json.uid
await fs.promises.writeFile(parentPath, JSON.stringify(parentJson))
}
}

View file

@ -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(/&gt;/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/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)

View file

@ -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()

View file

@ -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()

View file

@ -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 (
<MuiLink component={Link} to={`/${prefix}${type}/${name}`}>
{prefix}

View file

@ -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 (
<Table className={classes.table}>
<TableHead>
<TableRow>
{diff && <TableCell>Change</TableCell>}
<TableCell>Name</TableCell>
<TableCell>Type</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{obj.arguments.map((arg) => (
<TableRow key={arg.name} className={arg.className}>
{diff && (
<TableCell
className={clsx(
classes.changed,
classes[arg.changed!]
)}
>
{arg.changed}
</TableCell>
)}
<TableCell>
<code
className={
!arg.optional &&
arg.type !== '$FlagsBitField'
? classes.bold
: undefined
}
>
{arg.name}
</code>
</TableCell>
<TableCell className={classes.mono}>
{arg.optional ? (
<span title={arg.predicate}>
{LinkToTl(arg.type, history)}?
</span>
) : (
LinkToTl(arg.type, history)
)}
</TableCell>
<Description
description={arg.description}
component={TableCell}
/>
</TableRow>
))}
</TableBody>
</Table>
)
}

View file

@ -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 (
// <pre
// className={classes.code}
// dangerouslySetInnerHTML={{ __html: html }}
// />
// )
}

View file

@ -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({
<MuiLink href="https://github.com/teidesu/mtcute/tree/master/packages/tl-reference">
open-source
</MuiLink>{' '}
and licensed under MIT.<br/>
and licensed under MIT.
<br />
This website is not affiliated with Telegram.
</Typography>
</footer>
@ -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 (
<>
<div style={{ padding: '16px 32px' }}>
<MuiLink
component={Link}
to={`/${history ? 'history/' : ''}${type}/${name}`}
>
<Typography variant="h5" color="textPrimary">
{name}
</Typography>
</MuiLink>
<Description />
</div>
<Divider />
</>
)
}
export function Section({
title,
id,

View file

@ -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'

View file

@ -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 [<Link to={`/history/union/${s}`}>{code.identifier(s)}</Link>]
}
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(
<Link to={`/history/${inTypes ? 'class' : 'method'}/${fullName}`}>
{code.identifier(fullName)}
</Link>
)
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(
' = ',
<Link to={`/history/union/${type}`}>{code.identifier(type)}</Link>
)
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)
}

View file

@ -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) => (
<span className={classes.keyword}>{s}</span>
)
const identifier = (s: ReactNode) => (
<span className={classes.identifier}>{s}</span>
)
const property = (s: ReactNode) => (
<span className={classes.property}>{s}</span>
)
const comment = (s: ReactNode) => (
<span className={classes.comment}>{s}</span>
)
const string = (s: ReactNode) => <span className={classes.string}>{s}</span>
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) => <pre className={classes.code}>{s}</pre>
return {
keyword,
identifier,
property,
comment,
string,
typeName,
code,
}
}

View file

@ -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

View file

@ -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 = (
<Typography variant="body1" className={classes.paragraph}>
This type might no longer exist, but you could check{' '}
<MuiLink component={Link} to={`/history/${m[1]}`}>
History section
</MuiLink>
</Typography>
)
}
return (
<Page>
@ -20,6 +35,7 @@ const NotFoundPage = () => {
<Typography variant="body1" className={classes.paragraph}>
This page does not exist
</Typography>
{historyReference}
</Page>
)
}

View file

@ -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 (
<Page>
<Helmet>
<title>History</title>
</Helmet>
<div className={classes.heading1}>
<Typography variant="h3" id="tl-reference">
History
</Typography>
</div>
<Typography variant="body1" className={classes.paragraph}>
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.
<br />
<br />
Schemas are fetched automatically from <code>
tdesktop
</code>{' '}
repository, and older schemas (&lt;14) are fetched directly from
Telegram's website.
</Typography>
<Section id="schemas" title="Schemas">
{data.layers.nodes.map((layer) => (
<Typography variant="h5">
<MuiLink
component={Link}
to={`/history/layer${layer.layer}${
layer.rev ? `-rev${layer.rev}` : ''
}`}
>
Layer {layer.layer}
{layer.rev > 0 && (
<span className={classes.rev}>
{' '}
rev. {layer.rev}
</span>
)}
</MuiLink>
<small>
{' '}
(from{' '}
{layer.source.website
? 'website'
: layer.source.date}
)
</small>
</Typography>
))}
</Section>
</Page>
)
}
export const query = graphql`
query {
layers: allHistoryJson {
nodes {
layer
rev
source {
website
date(formatString: "DD-MM-YYYY")
commit
file
}
}
}
}
`

View file

@ -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 (
<Page
toc={[
@ -70,8 +76,17 @@ export default function IndexPage({ data }: { data: Data }) {
TL Reference
</Typography>
<Typography variant="body2">
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} /{' '}
<MuiLink
component={Link}
to={`/history/layer${currentLayer.layer}${
currentLayer.rev ? `-rev${currentLayer.rev}` : ''
}`}
>
view source
</MuiLink>
</Typography>
</div>
<Typography variant="body1" className={classes.paragraph}>
@ -284,6 +299,13 @@ export default function IndexPage({ data }: { data: Data }) {
)
})()}
</li>
<li>
History is available for{' '}
<MuiLink component={Link} to="/history">
<b>{data.historySchemas.totalCount}</b> schemas
</MuiLink>{' '}
and <b>{data.historyTypes.totalCount}</b> types
</li>
</Typography>
</Page>
)
@ -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
}
}
`

View file

@ -85,7 +85,11 @@ export default function NoDescriptionPage({ data }: { data: Data }) {
</TableCell>
<TableCell>{LinkToTl(node)}</TableCell>
<TableCell>
{(node.type === 'method' ? 'm_' : 'o_') +
{(node.type === 'method'
? 'm_'
: node.type === 'union'
? 'u_'
: 'o_') +
(node.prefix === 'mtproto/'
? 'mt_'
: '') +

View file

@ -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<string | undefined>(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 (
<Page>
<Helmet>
<title>
{`Layer ${layer.layer}` +
`${layer.rev > 0 ? ` rev. ${layer.rev}` : ''}`}
</title>
<meta
name="description"
content={
`TL code representing layer ${layer.layer}` +
`${layer.rev > 0 && ` rev. ${layer.rev}`}` +
` (from ${
layer.source.website ? 'website' : layer.source.date
})`
}
/>
</Helmet>
<div className={classes.navigation}>
{prev && (
<Button
component={Link}
variant="outlined"
color="primary"
to={`/history/layer${prev.layer}${
prev.rev ? `-rev${prev.rev}` : ''
}`}
startIcon={<ChevronLeftIcon />}
>
Layer {prev.layer}
{prev.rev > 0 && ` rev. ${prev.rev}`}
</Button>
)}
<Spacer />
{next && (
<Button
component={Link}
variant="outlined"
color="primary"
to={`/history/layer${next.layer}${
next.rev ? `-rev${next.rev}` : ''
}`}
endIcon={<ChevronRightIcon />}
>
Layer {next.layer}
{next.rev > 0 && ` rev. ${next.rev}`}
</Button>
)}
</div>
<div className={pageClasses.heading0}>
<Breadcrumbs>
<MuiLink component={Link} to={`/history`}>
History
</MuiLink>
<Typography color="textPrimary">
Layer {layer.layer}
{layer.rev > 0 && ` rev. ${layer.rev}`}
</Typography>
</Breadcrumbs>
<Typography variant="h3" id="title">
Layer {layer.layer}
{layer.rev > 0 && (
<span className={pageClasses.rev}>
{' '}
rev. {layer.rev}
</span>
)}
</Typography>
<Typography variant="body2">
from {layer.source.website ? 'website' : layer.source.date}
{!layer.source.website && (
<>
{' '}
/ commit{' '}
<MuiLink
href={`https://github.com/telegramdesktop/tdesktop/commit/${layer.source.commit}`}
target="_blank"
>
{layer.source.commit.substr(0, 7)}
</MuiLink>{' '}
(
<MuiLink
href={`https://github.com/telegramdesktop/tdesktop/blob/${layer.source.commit}/${layer.source.file}`}
target="_blank"
>
file
</MuiLink>
)
</>
)}
</Typography>
</div>
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={snackText !== undefined}
autoHideDuration={5000}
onClose={() => setSnackText(undefined)}
message={snackText}
/>
<div
className={classes.navigation}
style={{ justifyContent: 'flex-end' }}
>
<Button
className={classes.btn}
variant="outlined"
color="primary"
startIcon={<CodeIcon />}
onClick={copyToClipboard}
>
Copy to clipboard
</Button>
<Button
className={classes.btn}
variant="outlined"
color="primary"
startIcon={<CloudDownloadIcon />}
onClick={downloadAsFile}
>
Download
</Button>
</div>
<TlSchemaCode tl={layer.content} />
</Page>
)
}
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
}
}
`

View file

@ -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) =>
`<span class='${classes.keyword}'>${s}</span>`
const identifier = (s: string) =>
`<span class='${classes.identifier}'>${s}</span>`
const property = (s: string) =>
`<span class='${classes.property}'>${s}</span>`
const comment = (s: string) =>
`<span class='${classes.comment}'>${s}</span>`
const _string = (s: string) => `<span class='${classes.string}'>${s}</span>`
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 (
<pre
className={classes.code}
dangerouslySetInnerHTML={{ __html: s }}
/>
)
}
return (
<Page toc={toc}>
<Helmet>
@ -222,6 +157,17 @@ export default function TlObject({ data }: { data: GraphqlResult }) {
</>
)
)}
{obj.prefix === '' && (
<>
{' / '}
<MuiLink
component={Link}
to={`/history/${obj.type}/${obj.name}`}
>
history
</MuiLink>
</>
)}
</Typography>
</div>
<Description
@ -230,41 +176,7 @@ export default function TlObject({ data }: { data: GraphqlResult }) {
/>
{obj.type !== 'union' && (
<Section id="parameters" title="Parameters">
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Type</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{obj.arguments.map((arg) => (
<TableRow key={arg.name}>
<TableCell>
<code
className={
!arg.optional &&
arg.type !== '$FlagsBitField'
? classes.bold
: undefined
}
>
{arg.name}
</code>
</TableCell>
<TableCell className={classes.mono}>
{LinkToTl(arg.type)}
{arg.optional ? '?' : ''}
</TableCell>
<Description
description={arg.description}
component={TableCell}
/>
</TableRow>
))}
</TableBody>
</Table>
<ObjectParameters obj={obj} />
</Section>
)}
{obj.type === 'union' && (
@ -343,61 +255,9 @@ export default function TlObject({ data }: { data: GraphqlResult }) {
</Table>
</Section>
)}
<Typography
variant="h4"
id="typescript"
className={pageClasses.heading}
>
TypeScript declaration
</Typography>
{/* 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}'
)}
<Section id="typescript" title="TypeScript declaration">
<ObjectTsCode obj={obj} children={data.children?.nodes} />
</Section>
</Page>
)
}

View file

@ -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 = (
<>
<Typography variant="h5">Types</Typography>
<List>
{JSON.parse(item.diff).subtypes.map(
(type: string) => (
<ListItemTlLink
key={type}
type="class"
name={type}
history
/>
)
)}
</List>
</>
)
} else if (item.action === 'modified') {
let added = undefined
let removed = undefined
const diff = JSON.parse(item.diff).subtypes
if (diff.added.length) {
added = (
<>
<Typography variant="h5">Added</Typography>
<List>
{diff.added.map((type: string) => (
<ListItemTlLink
key={type}
type="class"
name={type}
history
/>
))}
</List>
</>
)
}
if (diff.removed.length) {
removed = (
<>
<Typography variant="h5">Removed</Typography>
<List>
{diff.removed.map((type: string) => (
<ListItemTlLink
key={type}
type="class"
name={type}
history
/>
))}
</List>
</>
)
}
content = (
<>
{added}
{removed}
</>
)
}
} else {
if (item.action === 'added') {
const object = JSON.parse(item.diff)
fillDescriptionFromCurrent(object)
content = (
<>
<Typography className={classes.description}>
Constructor ID: {hexConstructorId(object.id)}
<br />
{object.returns ? (
<>Returns: {LinkToTl(object.returns, true)}</>
) : (
<>Belongs to: {LinkToTl(object.type, true)}</>
)}
</Typography>
<Typography variant="h5">Parameters</Typography>
<ObjectParameters obj={object} history />
</>
)
} 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 = (
<Typography>
Constructor ID:{' '}
<span className={classes.fakeStrikethrough}>
{hexConstructorId(diff.id.old)}
</span>{' '}
{hexConstructorId(diff.id.new)}
</Typography>
)
}
if (diff.returns) {
returns = (
<Typography>
Returns:{' '}
<span className={classes.fakeStrikethrough}>
{LinkToTl(diff.returns.old, true)}
</span>{' '}
{LinkToTl(diff.returns.new, true)}
</Typography>
)
}
if (diff.type) {
union = (
<Typography>
Belongs to:{' '}
<span className={classes.fakeStrikethrough}>
{LinkToTl(diff.type.old, true)}
</span>{' '}
{LinkToTl(diff.type.new, true)}
</Typography>
)
}
content = (
<>
<Typography className={classes.description}>
{constructorId}
{returns}
{union}
</Typography>
<Typography variant="h5">Parameters</Typography>
{diff.arguments && (
<ObjectParameters obj={stub} diff history />
)}
</>
)
}
}
return (
<>
<div className={pageClasses.heading0}>
<Typography
variant="h4"
id={`layer${item.in.layer}${
item.in.rev ? `-rev${item.in.rev}` : ''
}`}
>
{capitalize(item.action)} in Layer {item.in.layer}
{item.in.rev > 0 && (
<span className={pageClasses.rev}>
{' '}
rev. {item.in.rev}
</span>
)}
</Typography>
<Typography variant="body2">
on {item.in.website ? 'website' : item.in.date}
{!item.in.website && (
<>
{' '}
/ commit{' '}
<MuiLink
href={`https://github.com/telegramdesktop/tdesktop/commit/${item.in.commit}`}
target="_blank"
>
{item.in.commit.substr(0, 7)}
</MuiLink>{' '}
(
<MuiLink
href={`https://github.com/telegramdesktop/tdesktop/blob/${item.in.commit}/${item.in.file}`}
target="_blank"
>
file
</MuiLink>
)
</>
)}
</Typography>
</div>
{content}
</>
)
}
return (
<Page toc={toc}>
<Helmet>
<title>History of {obj.name}</title>
<meta
name="description"
content={
`${obj.name}, first introduced in layer ${first.in.layer}` +
`, has had ${history.length - 1} changes over time`
}
/>
</Helmet>
<div className={pageClasses.heading0}>
<Breadcrumbs>
<MuiLink component={Link} to={`/history`}>
History
</MuiLink>
<Typography color="textPrimary">Types</Typography>
<Typography color="textPrimary">{obj.name}</Typography>
</Breadcrumbs>
<Typography variant="h3" id="title">
{obj.name}
</Typography>
<Typography variant="body2">
first introduced in layer {first.in.layer} on{' '}
{first.in.website ? 'website' : first.in.date}
{data.object && (
<>
{' '}
/{' '}
<MuiLink
component={Link}
to={`/${obj.type}/${obj.name}`}
>
current
</MuiLink>
</>
)}
</Typography>
</div>
<Description
description={obj.description}
className={classes.description}
/>
{history.map(HistoryItem)}
</Page>
)
}
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
}
}
}
`

View file

@ -17,6 +17,9 @@ export interface ExtendedTlObject {
type: string
predicate: string
description: string | null
changed?: 'added' | 'modified' | 'removed'
className?: string
}[]
throws: {
name: string

View file

@ -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')
}

View file

@ -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) {