From a197573258416e99ec173350e982719355ffbce0 Mon Sep 17 00:00:00 2001
From: Wizou
Date: Wed, 3 Nov 2021 03:53:48 +0100
Subject: [PATCH] Complete XML documentation of the Telegram API !
---
src/Generator.cs | 266 +-
src/TL.MTProto.cs | 70 +-
src/TL.Schema.cs | 10661 ++++++++++++++++++++++++++++++++++----------
src/TL.Secret.cs | 364 +-
4 files changed, 8926 insertions(+), 2435 deletions(-)
diff --git a/src/Generator.cs b/src/Generator.cs
index 49207d9..6fbb029 100644
--- a/src/Generator.cs
+++ b/src/Generator.cs
@@ -17,6 +17,8 @@ namespace WTelegram
readonly Dictionary knownStyles = new() { ["InitConnection"] = 1, ["Help_GetConfig"] = 0, ["HttpWait"] = -1 };
readonly Dictionary typeInfos = new();
readonly HashSet enumTypes = new();
+ readonly Dictionary enumValues = new();
+ readonly HashSet nullableCtor = new();
int currentLayer;
string tabIndent;
private string currentJson;
@@ -114,7 +116,7 @@ namespace WTelegram
ctorToTypes[ctor.ID] = ctor.layer == 0 ? structName : $"Layer{ctor.layer}.{structName}";
var typeInfo = typeInfos.GetOrCreate(ctor.type);
if (ctor.ID == 0x5BB8E511) { ctorToTypes[ctor.ID] = structName = ctor.predicate = ctor.type = "_Message"; }
- else if (ctor.ID == TL.Layer.NullCtor) { ctorToTypes[ctor.ID] += "=null"; typeInfo.Nullable = ctor; }
+ else if (ctor.ID == TL.Layer.NullCtor) { ctorToTypes[ctor.ID] += "=null"; typeInfo.Nullable = ctor; nullableCtor.Add(ctor.predicate); }
if (typeInfo.ReturnName == null) typeInfo.ReturnName = CSharpName(ctor.type);
typeInfo.Structs.Add(ctor);
if (structName == typeInfo.ReturnName) typeInfo.MainClass = ctor;
@@ -140,6 +142,7 @@ namespace WTelegram
typeInfo.Nullable = nullable[0];
typeInfo.Structs.Remove(typeInfo.Nullable);
ctorToTypes[typeInfo.Nullable.ID] += "=null";
+ nullableCtor.Add(typeInfo.Nullable.predicate);
}
if (typeInfo.MainClass == null)
{
@@ -191,7 +194,7 @@ namespace WTelegram
int autoPropsCount = 0;
foreach (var str in typeInfo.Structs)
{
- if (str.ID == 0 ||str.predicate.EndsWith("Empty") || str.predicate.EndsWith("TooLong") || str.predicate.EndsWith("NotModified")) continue;
+ if (str.ID == 0 || str.predicate.EndsWith("Empty") || str.predicate.EndsWith("TooLong") || str.predicate.EndsWith("NotModified")) continue;
for (int i = autoProps.Count - 1; i >= 0; i--)
if (!str.@params.Contains(autoProps[i]))
autoProps.RemoveAt(i);
@@ -202,6 +205,25 @@ namespace WTelegram
typeInfo.AutoProps = autoProps;
}
}
+ if (typeInfo.AsEnum && typeInfo.MainClass.id == null)
+ {
+ enumTypes.Add(typeInfo.ReturnName);
+ bool lowercase = typeInfo.ReturnName == "Storage_FileType";
+ string prefix = "";
+ while ((prefix += typeInfo.Structs[1].predicate[prefix.Length]) != null)
+ if (!typeInfo.Structs.All(ctor => ctor.id == null || ctor.predicate.StartsWith(prefix)))
+ break;
+ int prefixLen = CSharpName(prefix).Length - 1;
+ foreach (var ctor in typeInfo.Structs)
+ {
+ if (ctor.id == null) continue;
+ string className = CSharpName(ctor.predicate);
+ if (!allTypes.Add(className)) continue;
+ if (lowercase) className = className.ToLowerInvariant();
+ enumValues.Add(ctor.predicate, $"{typeInfo.ReturnName}.{className[prefixLen..]}");
+ ctorToTypes.Remove(ctor.ID);
+ }
+ }
}
var layers = schema.constructors.Select(c => c.layer).Distinct().ToList();
if (layers.Count > 1) // multi-layer file => generate abstract classes out of layer namespaces first
@@ -273,18 +295,21 @@ namespace WTelegram
if (needNewLine) { needNewLine = false; sw.WriteLine(); }
var parentClass = ctor == typeInfo.MainClass ? "ITLObject" : typeInfo.ReturnName;
var parms = ctor.@params;
+ var webDoc = WriteXmlDoc(sw, typeInfo, ctor);
if (ctorId == 0) // abstract parent
{
- if (currentJson != "TL.MTProto")
- sw.WriteLine($"{tabIndent}///See ");
- if (typeInfo.Nullable != null)
- sw.WriteLine($"{tabIndent}///a null value means {typeInfo.Nullable.predicate}");
if (typeInfo.AsEnum)
{
- WriteTypeAsEnum(sw, typeInfo);
+ WriteTypeAsEnum(sw, typeInfo, webDoc);
return;
}
sw.Write($"{tabIndent}public abstract partial class {ctor.predicate}");
+ if (webDoc != null && (parms.Length > 0 || typeInfo.AutoProps != null))
+ {
+ var len = Math.Max(parms.Length, typeInfo.AutoProps?.Count ?? 0);
+ var webDoc2 = ParseWebDoc($"constructor/{typeInfo.Structs.Skip(1).First(s => s.@params.Length >= len).predicate}");
+ webDoc["Parameters"] = webDoc2["Parameters"];
+ }
}
else
{
@@ -323,12 +348,7 @@ namespace WTelegram
}
}
if (currentJson != "TL.MTProto")
- {
- sw.WriteLine($"{tabIndent}///See ");
- if (typeInfo.Nullable != null && ctor == typeInfo.MainClass)
- sw.WriteLine($"{tabIndent}///a null value means {typeInfo.Nullable.predicate}");
sw.WriteLine($"{tabIndent}[TLDef(0x{ctor.ID:X8}{tldefReverse})]");
- }
else
{
sw.Write($"{tabIndent}[TLDef(0x{ctor.ID:X8}{tldefReverse})] //{ctor.predicate}#{ctor.ID:x8} ");
@@ -347,63 +367,64 @@ namespace WTelegram
commonFields = typeInfo.CommonFields;
continue;
}
+ var paramDoc = webDoc?.GetValueOrDefault("Parameters").table;
var hasFlagEnum = parms.Any(p => p.type.StartsWith("flags."));
- bool multiline = hasFlagEnum || parms.Length > 1 || typeInfo.AbstractUserOrChat || typeInfo.AutoProps != null;
- if (multiline)
+ sw.WriteLine();
+ sw.WriteLine(tabIndent + "{");
+ foreach (var parm in parms)
{
- sw.WriteLine();
- sw.WriteLine(tabIndent + "{");
+ if (parm.type.EndsWith("?true")) continue;
+ var doc = paramDoc?.GetValueOrDefault(parm.name);
+ if (doc != null) sw.WriteLine($"{tabIndent}\t/// {doc}");
+ if (parm.type == "#")
+ sw.WriteLine($"{tabIndent}\tpublic {(hasFlagEnum ? "Flags" : "int")} {parm.name};");
+ else
+ {
+ if (parm.type.StartsWith("flags."))
+ {
+ int qm = parm.type.IndexOf('?');
+ sw.WriteLine($"{tabIndent}\t[IfFlag({parm.type[6..qm]})] public {MapType(parm.type[(qm + 1)..], parm.name)} {MapName(parm.name)};");
+ }
+ else
+ sw.WriteLine($"{tabIndent}\tpublic {MapType(parm.type, parm.name)} {MapName(parm.name)};");
+ }
}
- else
- sw.Write(" { ");
if (hasFlagEnum)
{
+ sw.WriteLine();
var list = new SortedList();
+ var flagDoc = new Dictionary();
foreach (var parm in parms)
{
if (!parm.type.StartsWith("flags.") || !parm.type.EndsWith("?true")) continue;
var mask = 1 << int.Parse(parm.type[6..parm.type.IndexOf('?')]);
- if (!list.ContainsKey(mask)) list[mask] = MapName(parm.name);
+ if (list.ContainsKey(mask)) continue;
+ var doc = paramDoc?.GetValueOrDefault(parm.name);
+ if (doc != null) flagDoc[mask] = doc;
+ list[mask] = MapName(parm.name);
}
foreach (var parm in parms)
{
if (!parm.type.StartsWith("flags.") || parm.type.EndsWith("?true")) continue;
var mask = 1 << int.Parse(parm.type[6..parm.type.IndexOf('?')]);
if (list.ContainsKey(mask)) continue;
+ flagDoc[mask] = $"Field has a value";
var name = MapName("has_" + parm.name);
if (list.Values.Contains(name)) name += "_field";
list[mask] = name;
}
- string line = tabIndent + "\t[Flags] public enum Flags { ";
+ sw.WriteLine(tabIndent + "\t[Flags] public enum Flags");
+ sw.WriteLine(tabIndent + "\t{");
foreach (var (mask, name) in list)
{
- var str = $"{name} = 0x{mask:X}, ";
- if (line.Length + str.Length + tabIndent.Length * 3 >= 134) { sw.WriteLine(line); line = tabIndent + "\t\t"; }
- line += str;
+ if (flagDoc.TryGetValue(mask, out var summary)) sw.WriteLine($"{tabIndent}\t\t/// {summary}");
+ sw.WriteLine($"{tabIndent}\t\t{name} = 0x{mask:X},");
}
- sw.WriteLine(line.TrimEnd(',', ' ') + " }");
- }
- foreach (var parm in parms)
- {
- if (parm.type.EndsWith("?true")) continue;
- if (multiline) sw.Write(tabIndent + "\t");
- if (parm.type == "#")
- sw.Write($"public {(hasFlagEnum ? "Flags" : "int")} {parm.name};");
- else
- {
- if (parm.type.StartsWith("flags."))
- {
- int qm = parm.type.IndexOf('?');
- sw.Write($"[IfFlag({parm.type[6..qm]})] public {MapType(parm.type[(qm + 1)..], parm.name)} {MapName(parm.name)};");
- }
- else
- sw.Write($"public {MapType(parm.type, parm.name)} {MapName(parm.name)};");
- }
- if (multiline) sw.WriteLine();
+ sw.WriteLine(tabIndent + "\t}");
}
if (typeInfo.AutoProps != null)
{
- bool firstLine = parms.Length != 0;
+ bool firstLine = parms.Length != 0 || hasFlagEnum;
string format = $"{tabIndent}\tpublic ";
if (ctorId == 0)
format += "abstract {0} {1} {{ get; }}";
@@ -422,6 +443,8 @@ namespace WTelegram
string csName = CSharpName(parm.name);
if (csName.EndsWith("Id") && parm.type != "int" && parm.type != "long") csName = csName[..^2];
if (firstLine) { sw.WriteLine(); firstLine = false; }
+ var doc = paramDoc?.GetValueOrDefault(parm.name);
+ if (doc != null) sw.WriteLine($"{tabIndent}\t/// {doc}");
sw.WriteLine(string.Format(format, MapType(parm.type, parm.name), csName, value));
}
}
@@ -429,8 +452,10 @@ namespace WTelegram
if (hasUsersChats || (typeInfo.AbstractUserOrChat && (ctor == typeInfo.MainClass || parentClass == typeInfo.ReturnName)))
{
var modifier = !typeInfo.AbstractUserOrChat ? null : ctorId == 0 ? "abstract " : "override ";
+ bool withArg = !hasUsersChats || ctor.@params.Length != 3 || !parms.Contains(ParamPeer);
+ sw.WriteLine($"{tabIndent}\t/// returns a or for {(withArg ? "the given Peer" : "the result")}");
sw.Write($"{tabIndent}\tpublic {modifier}IPeerInfo UserOrChat");
- if (!hasUsersChats || ctor.@params.Length != 3 || !parms.Contains(ParamPeer))
+ if (withArg)
sw.Write("(Peer peer)");
if (modifier == "abstract ")
sw.WriteLine(";");
@@ -440,13 +465,131 @@ namespace WTelegram
sw.WriteLine(" => null;");
}
- if (multiline)
- sw.WriteLine(tabIndent + "}");
- else
- sw.WriteLine(" }");
+ sw.WriteLine(tabIndent + "}");
commonFields = typeInfo.CommonFields;
}
}
+
+ private Dictionary table)> WriteXmlDoc(StreamWriter sw, TypeInfo typeInfo, Constructor ctor)
+ {
+ if (currentJson == "TL.MTProto") return null;
+ var url = ctor.id == null ? $"type/{ctor.type}" : $"constructor/{ctor.predicate}";
+ var webDoc = ParseWebDoc(url);
+ var summary = webDoc?.GetValueOrDefault(ctor.predicate).descr;
+ var derived = webDoc?.GetValueOrDefault("Constructors").table;
+ if (derived != null && !typeInfo.AsEnum)
+ summary += $"\t\t
Derived classes: {string.Join(", ", derived.Keys.Where(k => k != ""))}";
+ summary += $"\t\t
See ";
+ sw.WriteLine($"{tabIndent}/// {summary.Trim()}");
+ if (typeInfo.Nullable != null && ctor == typeInfo.MainClass)
+ sw.WriteLine($"{tabIndent}/// a null value means {typeInfo.Nullable.predicate}");
+ return webDoc;
+ }
+
+ private void WriteXmlDoc(StreamWriter sw, Method method)
+ {
+ if (currentJson == "TL.MTProto") return;
+ var webDoc = ParseWebDoc($"method/{method.method}");
+ var summary = webDoc?.GetValueOrDefault(method.method).descr;
+ var paramDoc = webDoc?.GetValueOrDefault("Parameters").table;
+ var excepDoc = webDoc?.GetValueOrDefault("Possible errors").table;
+ summary += $"\t\t
See ";
+ sw.WriteLine($"{tabIndent}/// {summary.Trim()}");
+ if (paramDoc != null)
+ foreach (var (name, doc) in paramDoc)
+ if (name != "flags")
+ sw.WriteLine($"{tabIndent}/// {doc}");
+ if (typeInfos.GetValueOrDefault(method.type)?.Nullable is Constructor nullable)
+ sw.WriteLine($"{tabIndent}/// a null value means {nullable.predicate}");
+ }
+
+ ///
+ private Dictionary table)> ParseWebDoc(string url)
+ {
+ var path = $@"{Environment.GetEnvironmentVariable("telegram-crawler")}\data\corefork.telegram.org\{url}";
+ if (!File.Exists(path))
+ if (!File.Exists(path += ".html"))
+ return null;
+ var result = new Dictionary table)>();
+ var html = File.ReadAllText(path);
+ foreach (var section in html[html.IndexOf("" }, StringSplitOptions.None))
+ {
+ var index = 0;
+ do { index = section.IndexOf('>', index) + 1; } while (section[index] == '<');
+ var title = section[index..section.IndexOf("")) >= 0)
+ {
+ descr = rest[(index + 3)..rest.IndexOf("
", index)];
+ rest = rest[(index + 7 + descr.Length)..].Trim();
+ descr = ProcessDescr(descr);
+ }
+ Dictionary table = null;
+ if (rest.StartsWith("