TL Functions are now direct Client methods ; Renamed/rationalized serialization methods

This commit is contained in:
Wizou 2021-08-09 11:41:50 +02:00
parent 8ae3c2a283
commit 674130e079
8 changed files with 4166 additions and 2589 deletions

View file

@ -1,5 +1,5 @@
[![NuGet version](https://badge.fury.io/nu/WTelegramClient.svg)](https://badge.fury.io/nu/WTelegramClient)
[![Build Status](https://dev.azure.com/wiz0u/WTelegramClient/_apis/build/status/wiz0u.WTelegramClient?branchName=master)](https://dev.azure.com/wiz0u/WTelegramClient/_build/latest?definitionId=7&branchName=master)
[![Build Status](https://dev.azure.com/wiz0u/WTelegramClient/_apis/build/status/wiz0u.WTelegramClient?branchName=master)](https://dev.azure.com/wiz0u/WTelegramClient/_packaging?_a=package&feed=WTelegramClient&package=WTelegramClient&protocolType=NuGet)
# WTelegramClient
### _Telegram client library written 100% in C# and .NET Core_
@ -108,7 +108,7 @@ Here are the main expected developments:
- [x] Support SignUp of unregistered users
- [x] Improve code Generator (import of TL-schema JSONs)
- [x] Nuget deployment & public CI feed
- [ ] Convert API functions classes to real methods and serialize structures without using Reflection
- [x] Convert API functions classes to real methods and serialize structures without using Reflection
- [ ] Separate task/thread for reading/handling update messages independently from CallAsync
- [ ] Support MTProto 2.0
- [ ] Support users with 2FA enabled

View file

@ -16,7 +16,7 @@ using static WTelegram.Encryption;
namespace WTelegram
{
public sealed class Client : IDisposable
public sealed partial class Client : IDisposable
{
public Config TLConfig { get; private set; }
private readonly Func<string, string> _config;
@ -27,7 +27,7 @@ namespace WTelegram
private TcpClient _tcpClient;
private NetworkStream _networkStream;
private int _frame_seqTx = 0, _frame_seqRx = 0;
private ITLObject _lastSentMsg;
private ITLFunction<object> _lastSentMsg;
private Type _lastRpcResultType = typeof(object);
private readonly List<long> _msgsToAck = new();
private int _unexpectedSaltChange;
@ -93,21 +93,15 @@ namespace WTelegram
if (_session.AuthKey == null)
await CreateAuthorizationKey(this, _session);
TLConfig = await CallAsync(new Fn.InvokeWithLayer<Config>
{
layer = Schema.Layer,
query = new Fn.InitConnection<Config>
{
api_id = _apiId,
device_model = Config("device_model"),
system_version = Config("system_version"),
app_version = Config("app_version"),
system_lang_code = Config("system_lang_code"),
lang_pack = Config("lang_pack"),
lang_code = Config("lang_code"),
query = new Fn.Help_GetConfig()
}
});
TLConfig = await InvokeWithLayer(Schema.Layer,
InitConnection<Config>(_apiId,
Config("device_model"),
Config("system_version"),
Config("app_version"),
Config("system_lang_code"),
Config("lang_pack"),
Config("lang_code"),
Help_GetConfig));
}
private async Task MigrateDCAsync(int dcId)
@ -115,7 +109,7 @@ namespace WTelegram
Helpers.Log(2, $"Migrate to DC {dcId}...");
Auth_ExportedAuthorization exported = null;
if (_session.User != null)
exported = await CallAsync(new Fn.Auth_ExportAuthorization { dc_id = dcId });
exported = await Auth_ExportAuthorization(dcId);
var prevFamily = _tcpClient.Client.RemoteEndPoint.AddressFamily;
_tcpClient.Close();
var dcOptions = TLConfig.dc_options.Where(dc => dc.id == dcId && (dc.flags & (DcOption.Flags.media_only | DcOption.Flags.cdn)) == 0);
@ -128,10 +122,10 @@ namespace WTelegram
await ConnectAsync();
if (exported != null)
{
var authorization = await CallAsync(new Fn.Auth_ImportAuthorization { id = exported.id, bytes = exported.bytes });
var authorization = await Auth_ImportAuthorization(exported.id, exported.bytes);
if (authorization is not Auth_Authorization { user: User user })
throw new ApplicationException("Failed to get Authorization: " + authorization.GetType().Name);
_session.User = Schema.Serialize(user);
_session.User = user.Serialize();
}
}
@ -142,7 +136,14 @@ namespace WTelegram
_tcpClient?.Dispose();
}
public async Task SendAsync(ITLObject msg, bool isContent = true)
public Task SendAsync(ITLObject msg, bool isContent = true)
=> SendAsync<object>(writer =>
{
writer.WriteTLObject(msg);
return msg.GetType().Name;
}, isContent);
public async Task SendAsync<X>(ITLFunction<X> msgSerializer, bool isContent = true)
{
if (_session.AuthKeyID != 0) await CheckMsgsToAck();
using var memStream = new MemoryStream(1024);
@ -150,20 +151,19 @@ namespace WTelegram
writer.Write(0); // int32 frame_len (to be patched with full frame length)
writer.Write(_frame_seqTx++); // int32 frame_seq
(long msgId, int seqno) = _session.NewMsg(isContent);
(long msgId, int seqno) = _session.NewMsg(isContent && _session.AuthKeyID != 0);
if (_session.AuthKeyID == 0) // send unencrypted message
{
Helpers.Log(1, $"Sending {msg.GetType().Name}...");
writer.Write(0L); // int64 auth_key_id = 0 (Unencrypted)
writer.Write(msgId); // int64 message_id
writer.Write(0); // int32 message_data_length (to be patched)
Schema.Serialize(writer, msg); // bytes message_data
writer.Write(0L); // int64 auth_key_id = 0 (Unencrypted)
writer.Write(msgId); // int64 message_id
writer.Write(0); // int32 message_data_length (to be patched)
var typeName = msgSerializer(writer); // bytes message_data
Helpers.Log(1, $"Sending {typeName}...");
BinaryPrimitives.WriteInt32LittleEndian(memStream.GetBuffer().AsSpan(24), (int)memStream.Length - 28); // patch message_data_length
}
else
{
Helpers.Log(1, $"Sending {msg.GetType().Name,-50} #{(short)msgId.GetHashCode():X4}");
//TODO: implement MTProto 2.0
using var clearStream = new MemoryStream(1024);
using var clearWriter = new BinaryWriter(clearStream, Encoding.UTF8);
@ -172,7 +172,8 @@ namespace WTelegram
clearWriter.Write(msgId); // int64 message_id
clearWriter.Write(seqno); // int32 msg_seqno
clearWriter.Write(0); // int32 message_data_length (to be patched)
Schema.Serialize(clearWriter, msg); // bytes message_data
var typeName = msgSerializer(clearWriter); // bytes message_data
Helpers.Log(1, $"Sending {typeName,-50} #{(short)msgId.GetHashCode():X4}");
int clearLength = (int)clearStream.Length; // length before padding (= 32 + message_data_length)
int padding = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + padding);
@ -197,7 +198,7 @@ namespace WTelegram
//TODO: support Transport obfuscation?
await _networkStream.WriteAsync(frame);
_lastSentMsg = msg;
_lastSentMsg = writer => msgSerializer(writer);
}
internal async Task<ITLObject> RecvInternalAsync()
@ -221,11 +222,8 @@ namespace WTelegram
int length = reader.ReadInt32();
if (length != data.Length - 20) throw new ApplicationException($"Unexpected unencrypted length {length} != {data.Length - 20}");
var ctorNb = reader.ReadUInt32();
if (!Schema.Table.TryGetValue(ctorNb, out var realType))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
Helpers.Log(1, $"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} unencrypted");
return Schema.DeserializeObject(reader, realType);
return reader.ReadTLObject((type, _) =>
Helpers.Log(1, $"Receiving {type.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} unencrypted"));
}
else if (authKeyId != _session.AuthKeyID)
throw new ApplicationException($"Received a packet encrypted with unexpected key {authKeyId:X}");
@ -255,15 +253,13 @@ namespace WTelegram
if (!data.AsSpan(8, 16).SequenceEqual(SHA1.HashData(decrypted_data.AsSpan(0, 32 + length)).AsSpan(4)))
throw new ApplicationException($"Mismatch between MsgKey & decrypted SHA1");
var ctorNb = reader.ReadUInt32();
if (!Schema.Table.TryGetValue(ctorNb, out var realType))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
Helpers.Log(1, $"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} {(seqno == -1 ? "clearText" : "isContent")}={(seqno & 1) != 0}");
if (realType == typeof(RpcResult))
return DeserializeRpcResult(reader); // necessary hack because some RPC return bare types like bool or int[]
else
return Schema.DeserializeObject(reader, realType);
}
return reader.ReadTLObject((type, obj) =>
{
Helpers.Log(1, $"Receiving {type.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} {(seqno == -1 ? "clearText" : "isContent")}={(seqno & 1) != 0}");
if (type == typeof(RpcResult))
DeserializeRpcResult(reader, (RpcResult)obj); // necessary hack because some RPC return bare types like bool or int[]
});
}
static string TransportError(int error_code) => error_code switch
{
@ -310,16 +306,13 @@ namespace WTelegram
return length;
}
private RpcResult DeserializeRpcResult(BinaryReader reader)
private void DeserializeRpcResult(BinaryReader reader, RpcResult rpcResult)
{
long reqMsgId = reader.ReadInt64();
var rpcResult = new RpcResult { req_msg_id = reqMsgId };
if (reqMsgId == _session.LastSentMsgId)
rpcResult.result = Schema.DeserializeValue(reader, _lastRpcResultType);
if ((rpcResult.req_msg_id = reader.ReadInt64()) == _session.LastSentMsgId)
rpcResult.result = reader.ReadTLValue(_lastRpcResultType);
else
rpcResult.result = Schema.Deserialize<ITLObject>(reader);
Helpers.Log(1, $" → {rpcResult.result.GetType().Name,-50} #{(short)reqMsgId.GetHashCode():X4}");
return rpcResult;
rpcResult.result = reader.ReadTLObject();
Helpers.Log(1, $" → {rpcResult.result.GetType().Name,-50} #{(short)rpcResult.req_msg_id.GetHashCode():X4}");
}
public class RpcException : Exception
@ -328,16 +321,17 @@ namespace WTelegram
public RpcException(int code, string message) : base(message) => Code = code;
}
public async Task<X> CallAsync<X>(ITLFunction<X> request)
public async Task<X> CallAsync<X>(ITLFunction<X> request)
{
await SendAsync(request);
// TODO: create a background reactor system that handles incoming packets and wake up awaiting tasks when their result has arrived
// This would allow parallelization of Send task and avoid the risk of calling RecvInternal concurrently
_lastRpcResultType = typeof(X);
for (; ;)
for (; ;) //TODO: implement a timeout
{
var reply = await RecvInternalAsync();
if (reply is RpcResult rpcResult && rpcResult.req_msg_id == _session.LastSentMsgId)
if (reply is X plainResult) return plainResult;
else if (reply is RpcResult rpcResult && rpcResult.req_msg_id == _session.LastSentMsgId)
{
if (rpcResult.result is RpcError rpcError)
{
@ -409,24 +403,13 @@ namespace WTelegram
if (_session.User != null)
return Schema.Deserialize<User>(_session.User);
string phone_number = Config("phone_number");
var sentCode = await CallAsync(new Fn.Auth_SendCode
{
phone_number = phone_number,
api_id = _apiId,
api_hash = _apiHash,
settings = settings ?? new()
});
var sentCode = await Auth_SendCode(phone_number, _apiId, _apiHash, settings ?? new());
Helpers.Log(3, $"A verification code has been sent via {sentCode.type.GetType().Name[17..]}");
var verification_code = Config("verification_code");
Auth_AuthorizationBase authorization;
try
{
authorization = await CallAsync(new Fn.Auth_SignIn
{
phone_number = phone_number,
phone_code_hash = sentCode.phone_code_hash,
phone_code = verification_code
});
authorization = await Auth_SignIn(phone_number, sentCode.phone_code_hash, verification_code);
}
catch (RpcException e) when (e.Code == 400 && e.Message == "SESSION_PASSWORD_NEEDED")
{
@ -437,20 +420,15 @@ namespace WTelegram
var waitUntil = DateTime.UtcNow.AddSeconds(3);
if (signUpRequired.terms_of_service != null && _updateHandler != null)
await _updateHandler?.Invoke(signUpRequired.terms_of_service); // give caller the possibility to read and accept TOS
var signUp = new Fn.Auth_SignUp
{
phone_number = phone_number,
phone_code_hash = sentCode.phone_code_hash,
first_name = Config("first_name"),
last_name = Config("last_name"),
};
var first_name = Config("first_name");
var last_name = Config("last_name");
var wait = waitUntil - DateTime.UtcNow;
if (wait > TimeSpan.Zero) await Task.Delay(wait); // we get a FLOOD_WAIT_3 if we SignUp too fast
authorization = await CallAsync(signUp);
authorization = await Auth_SignUp(phone_number, sentCode.phone_code_hash, first_name, last_name);
}
if (authorization is not Auth_Authorization { user: User user })
throw new ApplicationException("Failed to get Authorization: " + authorization.GetType().Name);
_session.User = Schema.Serialize(user);
_session.User = user.Serialize();
_session.Save();
return user;
}
@ -477,10 +455,13 @@ namespace WTelegram
{
//TODO: parallelize several parts sending through a N-semaphore? (needs a reactor first)
read = await FullReadAsync(stream, bytes, (int)Math.Min(partSize, bytesLeft));
await CallAsync<bool>(isBig
? new Fn.Upload_SaveBigFilePart { bytes = bytes, file_id = file_id, file_part = file_part, file_total_parts = file_total_parts }
: new Fn.Upload_SaveFilePart { bytes = bytes, file_id = file_id, file_part = file_part });
if (!isBig) md5.TransformBlock(bytes, 0, read, null, 0);
if (isBig)
await Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
else
{
await Upload_SaveFilePart(file_id, file_part, bytes);
md5.TransformBlock(bytes, 0, read, null, 0);
}
bytesLeft -= read;
if (read < partSize && bytesLeft != 0) throw new ApplicationException($"Failed to fully read stream ({read},{bytesLeft})");
}
@ -524,38 +505,12 @@ namespace WTelegram
/// <param name="media">media specification or <see langword="null"/></param>
public Task<UpdatesBase> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool disable_preview = false)
{
ITLFunction<UpdatesBase> request = (media == null)
? new Fn.Messages_SendMessage
{
flags = GetFlags(),
peer = peer,
reply_to_msg_id = reply_to_msg_id,
message = text,
random_id = Helpers.RandomLong(),
entities = entities,
schedule_date = schedule_date
}
: new Fn.Messages_SendMedia
{
flags = (Fn.Messages_SendMedia.Flags)GetFlags(),
peer = peer,
reply_to_msg_id = reply_to_msg_id,
media = media,
message = text,
random_id = Helpers.RandomLong(),
entities = entities,
schedule_date = schedule_date
};
return CallAsync(request);
Fn.Messages_SendMessage.Flags GetFlags()
{
return ((reply_to_msg_id != 0) ? Fn.Messages_SendMessage.Flags.has_reply_to_msg_id : 0)
| (disable_preview ? Fn.Messages_SendMessage.Flags.no_webpage : 0)
// | (reply_markup != null ? Messages_SendMessage.Flags.has_reply_markup : 0)
| (entities != null ? Fn.Messages_SendMessage.Flags.has_entities : 0)
| (schedule_date != default ? Fn.Messages_SendMessage.Flags.has_schedule_date : 0);
}
if (media == null)
return Messages_SendMessage(peer, text, Helpers.RandomLong(),
no_webpage: disable_preview, reply_to_msg_id: reply_to_msg_id, entities: entities, schedule_date: schedule_date);
else
return Messages_SendMedia(peer, media, text, Helpers.RandomLong(),
reply_to_msg_id: reply_to_msg_id, entities: entities, schedule_date: schedule_date);
}
#endregion
}

View file

@ -21,14 +21,13 @@ namespace WTelegram
if (PublicKeys.Count == 0) LoadDefaultPublicKey();
//1)
var reqPQ = new Fn.ReqPQ() { nonce = new Int128(RNG) };
await client.SendAsync(reqPQ, false);
var nonce = new Int128(RNG);
var resPQ = await client.ReqPQ(nonce);
//2)
var reply = await client.RecvInternalAsync();
if (reply is not ResPQ resPQ) throw new ApplicationException($"Expected ResPQ but got {reply.GetType().Name}");
if (resPQ.nonce != reqPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (resPQ.nonce != nonce) throw new ApplicationException("Nonce mismatch");
var fingerprint = resPQ.server_public_key_fingerprints.FirstOrDefault(PublicKeys.ContainsKey);
if (fingerprint == 0) throw new ApplicationException("Couldn't match any server_public_key_fingerprints");
var publicKey = PublicKeys[fingerprint];
Helpers.Log(2, $"Selected public key with fingerprint {fingerprint:X}");
//3)
long retry_id = 0;
@ -36,72 +35,92 @@ namespace WTelegram
ulong p = Helpers.PQFactorize(pq);
ulong q = pq / p;
//4)
var new_nonce = new Int256(RNG);
var reqDHparams = MakeReqDHparam(fingerprint, PublicKeys[fingerprint], new PQInnerData
var pqInnerData = new PQInnerData
{
pq = resPQ.pq,
p = Helpers.ToBigEndian(p),
q = Helpers.ToBigEndian(q),
nonce = resPQ.nonce,
nonce = nonce,
server_nonce = resPQ.server_nonce,
new_nonce = new_nonce,
});
await client.SendAsync(reqDHparams, false);
new_nonce = new Int256(RNG),
};
byte[] encrypted_data;
{ // the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(255);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
writer.WriteTLObject(pqInnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
if (clearLength > 255) throw new ApplicationException("PQInnerData too big");
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, 255 - clearLength);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer); // patch with SHA1
encrypted_data = BigInteger.ModPow(new BigInteger(clearBuffer, true, true), // encrypt with RSA key
new BigInteger(publicKey.e, true, true), new BigInteger(publicKey.n, true, true)).ToByteArray(true, true);
}
var serverDHparams = await client.ReqDHParams(pqInnerData.nonce, pqInnerData.server_nonce, pqInnerData.p, pqInnerData.q, fingerprint, encrypted_data);
//5)
reply = await client.RecvInternalAsync();
if (reply is not ServerDHParams serverDHparams) throw new ApplicationException($"Expected ServerDHParams but got {reply.GetType().Name}");
var localTime = DateTimeOffset.UtcNow;
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new ApplicationException("not server_DH_params_ok");
if (serverDHparamsOk.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHparamsOk.nonce != nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(resPQ.server_nonce, new_nonce);
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(resPQ.server_nonce, pqInnerData.new_nonce);
var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false);
using var encryptedReader = new BinaryReader(new MemoryStream(answer));
var answerHash = encryptedReader.ReadBytes(20);
var answerObj = Schema.DeserializeValue(encryptedReader, typeof(object));
var answerObj = encryptedReader.ReadTLObject();
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new ApplicationException("not server_DH_inner_data");
long padding = encryptedReader.BaseStream.Length - encryptedReader.BaseStream.Position;
if (padding >= 16) throw new ApplicationException("Too much pad");
if (!Enumerable.SequenceEqual(SHA1.HashData(answer.AsSpan(20..^(int)padding)), answerHash))
throw new ApplicationException("Answer SHA1 mismatch");
if (serverDHinnerData.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHinnerData.nonce != nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var g_a = new BigInteger(serverDHinnerData.g_a, true, true);
var dh_prime = new BigInteger(serverDHinnerData.dh_prime, true, true);
ValidityChecks(dh_prime, serverDHinnerData.g);
Helpers.Log(1, $"Server time: {serverDHinnerData.server_time} UTC");
session.ServerTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
//6)
var bData = new byte[256];
RNG.GetBytes(bData);
var b = new BigInteger(bData, true, true);
var g_b = BigInteger.ModPow(serverDHinnerData.g, b, dh_prime);
var setClientDHparams = MakeClientDHparams(tmp_aes_key, tmp_aes_iv, new ClientDHInnerData
var clientDHinnerData = new ClientDHInnerData
{
nonce = resPQ.nonce,
nonce = nonce,
server_nonce = resPQ.server_nonce,
retry_id = retry_id,
g_b = g_b.ToByteArray(true, true)
});
await client.SendAsync(setClientDHparams, false);
};
{ // the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(384);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
writer.WriteTLObject(clientDHinnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
int paddingToAdd = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + paddingToAdd);
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, paddingToAdd);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer);
encrypted_data = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(0, clearLength + paddingToAdd), tmp_aes_key, tmp_aes_iv, true);
}
var setClientDHparamsAnswer = await client.SetClientDHParams(clientDHinnerData.nonce, clientDHinnerData.server_nonce, encrypted_data);
//7)
var gab = BigInteger.ModPow(g_a, b, dh_prime);
var authKey = gab.ToByteArray(true, true);
//8)
var authKeyHash = SHA1.HashData(authKey);
retry_id = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash); // (auth_key_aux_hash)
//9)
reply = await client.RecvInternalAsync();
if (reply is not SetClientDHParamsAnswer setClientDHparamsAnswer) throw new ApplicationException($"Expected SetClientDHParamsAnswer but got {reply.GetType().Name}");
if (setClientDHparamsAnswer is not DHGenOk) throw new ApplicationException("not dh_gen_ok");
if (setClientDHparamsAnswer.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (setClientDHparamsAnswer.nonce != nonce) throw new ApplicationException("Nonce mismatch");
if (setClientDHparamsAnswer.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var expected_new_nonceN = new byte[32 + 1 + 8];
new_nonce.raw.CopyTo(expected_new_nonceN, 0);
pqInnerData.new_nonce.raw.CopyTo(expected_new_nonceN, 0);
expected_new_nonceN[32] = 1;
Array.Copy(authKeyHash, 0, expected_new_nonceN, 33, 8); // (auth_key_aux_hash)
if (!Enumerable.SequenceEqual(setClientDHparamsAnswer.new_nonce_hashN.raw, SHA1.HashData(expected_new_nonceN).Skip(4)))
@ -109,7 +128,7 @@ namespace WTelegram
session.AuthKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12));
session.AuthKey = authKey;
session.Salt = BinaryPrimitives.ReadInt64LittleEndian(new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
session.Salt = BinaryPrimitives.ReadInt64LittleEndian(pqInnerData.new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
session.Save();
static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(Int128 server_nonce, Int256 new_nonce)
@ -153,62 +172,13 @@ namespace WTelegram
// We recommend checking that g_a and g_b are between 2^{2048-64} and dh_prime - 2^{2048-64} as well.
}
private static Fn.ReqDHParams MakeReqDHparam(long publicKey_fingerprint, RSAPublicKey publicKey, PQInnerData pqInnerData)
{
// the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(255);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
Schema.Serialize(writer, pqInnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
if (clearLength > 255) throw new ApplicationException("PQInnerData too big");
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, 255 - clearLength);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer); // patch with SHA1
var encrypted_data = BigInteger.ModPow(new BigInteger(clearBuffer, true, true), // encrypt with RSA key
new BigInteger(publicKey.e, true, true), new BigInteger(publicKey.n, true, true)).ToByteArray(true, true);
return new Fn.ReqDHParams
{
nonce = pqInnerData.nonce,
server_nonce = pqInnerData.server_nonce,
p = pqInnerData.p,
q = pqInnerData.q,
public_key_fingerprint = publicKey_fingerprint,
encrypted_data = encrypted_data
};
}
private static Fn.SetClientDHParams MakeClientDHparams(byte[] tmp_aes_key, byte[] tmp_aes_iv, ClientDHInnerData clientDHinnerData)
{
// the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(384);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
Schema.Serialize(writer, clientDHinnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
int padding = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + padding);
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, padding);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer);
var encrypted_data = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(0, clearLength + padding), tmp_aes_key, tmp_aes_iv, true);
return new Fn.SetClientDHParams
{
nonce = clientDHinnerData.nonce,
server_nonce = clientDHinnerData.server_nonce,
encrypted_data = encrypted_data
};
}
public static void LoadPublicKey(string pem)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(pem);
var rsaParam = rsa.ExportParameters(false);
var publicKey = new RSAPublicKey { n = rsaParam.Modulus, e = rsaParam.Exponent };
var bareData = Schema.Serialize(publicKey).AsSpan(4); // bare serialization
var bareData = publicKey.Serialize().AsSpan(4); // bare serialization
var fingerprint = BinaryPrimitives.ReadInt64LittleEndian(SHA1.HashData(bareData).AsSpan(12)); // 64 lower-order bits of SHA1
PublicKeys[fingerprint] = publicKey;
Helpers.Log(1, $"Loaded a public key with fingerprint {fingerprint:X}");

View file

@ -16,6 +16,7 @@ namespace WTelegram
readonly Dictionary<int, string> ctorToTypes = new();
readonly HashSet<string> allTypes = new();
readonly Dictionary<int, Dictionary<string, TypeInfo>> typeInfosByLayer = new();
readonly Dictionary<string, int> knownStyles = new() { ["InitConnection"] = 1, ["Help_GetConfig"] = 0, ["HttpWait"] = -1 };
Dictionary<string, TypeInfo> typeInfos;
int currentLayer;
string tabIndent;
@ -30,18 +31,20 @@ namespace WTelegram
//await File.WriteAllBytesAsync("TL.MTProto.json", await http.GetByteArrayAsync("https://core.telegram.org/schema/mtproto-json"));
//await File.WriteAllBytesAsync("TL.Schema.json", await http.GetByteArrayAsync("https://core.telegram.org/schema/json"));
//await File.WriteAllBytesAsync("TL.Secret.json", await http.GetByteArrayAsync("https://core.telegram.org/schema/end-to-end-json"));
FromJson("TL.MTProto.json", "TL.MTProto.cs", @"TL.Table.cs");
FromJson("TL.MTProto.json", "TL.MTProto.cs", @"TL.Table.cs", true);
FromJson("TL.Schema.json", "TL.Schema.cs", @"TL.Table.cs");
FromJson("TL.Secret.json", "TL.Secret.cs", @"TL.Table.cs");
}
public void FromJson(string jsonPath, string outputCs, string tableCs = null)
public void FromJson(string jsonPath, string outputCs, string tableCs = null, bool legacy = false)
{
Console.WriteLine("Parsing " + jsonPath);
var schema = JsonSerializer.Deserialize<SchemaJson>(File.ReadAllText(jsonPath));
if (legacy) InjectLegacy(schema);
using var sw = File.CreateText(outputCs);
sw.WriteLine("// This file is (mainly) generated automatically using the Generator class");
sw.WriteLine("using System;");
if (schema.methods.Count != 0) sw.WriteLine("using System.Threading.Tasks;");
sw.WriteLine();
sw.WriteLine("namespace TL");
sw.Write("{");
@ -107,7 +110,7 @@ namespace WTelegram
}
}
foreach (var typeInfo in typeInfos.Values)
WriteTypeInfo(sw, typeInfo, jsonPath, layerPrefix, false);
WriteTypeInfo(sw, typeInfo, layerPrefix, false);
if (layer.Key != 0)
{
sw.WriteLine("\t}");
@ -116,20 +119,28 @@ namespace WTelegram
}
if (typeInfosByLayer[0]["Message"].SameName.ID == 0x5BB8E511) typeInfosByLayer[0].Remove("Message");
sw.WriteLine();
var methods = new List<TypeInfo>();
if (schema.methods.Length != 0)
if (schema.methods.Count != 0)
{
typeInfos = typeInfosByLayer[0];
sw.WriteLine("\tpublic static partial class Fn // ---functions---");
sw.WriteLine("}");
sw.WriteLine("");
sw.WriteLine("namespace WTelegram\t\t// ---functions---");
sw.WriteLine("{");
sw.WriteLine("\tusing System.IO;");
sw.WriteLine("\tusing TL;");
sw.WriteLine();
sw.WriteLine("\tpublic partial class Client");
//sw.WriteLine("\tpublic static partial class Fn // ---functions---");
sw.Write("\t{");
tabIndent = "\t\t";
foreach (var method in schema.methods)
{
var typeInfo = new TypeInfo { ReturnName = method.type };
typeInfo.Structs.Add(new Constructor { id = method.id, @params = method.@params, predicate = method.method, type = method.type });
methods.Add(typeInfo);
WriteTypeInfo(sw, typeInfo, jsonPath, "", true);
WriteMethod(sw, method);
//var typeInfo = new TypeInfo { ReturnName = method.type };
//typeInfo.Structs.Add(new Constructor { id = method.id, @params = method.@params, predicate = method.method, type = method.type });
//methods.Add(typeInfo);
//WriteTypeInfo(sw, typeInfo, "", true);
}
sw.WriteLine("\t}");
}
@ -138,7 +149,35 @@ namespace WTelegram
if (tableCs != null) UpdateTable(jsonPath, tableCs, methods);
}
void WriteTypeInfo(StreamWriter sw, TypeInfo typeInfo, string definedIn, string layerPrefix, bool isMethod)
private static void InjectLegacy(SchemaJson schema)
{
foreach (var c in schema.constructors.Where(c => c.type == "P_Q_inner_data"))
c.predicate = c.predicate[..^2] + "DC";
var add = new Constructor { id = ID(0x83C95AEC), predicate = "p_q_inner_data", type = "P_Q_inner_data",
@params = Params("pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256") };
schema.constructors.Insert(schema.constructors.FindIndex(c => c.type == add.type), add);
add = new Constructor { id = ID(0x79CB045D), predicate = "server_DH_params_fail", type = "Server_DH_Params",
@params = Params("nonce:int128 server_nonce:int128 new_nonce_hash:int128") };
schema.constructors.Insert(schema.constructors.FindIndex(c => c.type == add.type), add);
add = new Constructor { id = ID(0x7A19CB76), predicate = "RSA_public_key", type = "RSAPublicKey",
@params = Params("n:bytes e:bytes") };
schema.constructors.Insert(schema.constructors.FindIndex(c => c.type == "DestroyAuthKeyRes"), add);
foreach (var c in schema.constructors.Where(c => c.type == "Set_client_DH_params_answer"))
{
c.predicate = "DH" + c.predicate[2..];
c.@params[2].name = "new_nonce_hashN";
}
schema.constructors.Find(c => c.predicate == "future_salts").@params[2].type = "Vector<FutureSalt>";
var addm = new Method { id = ID(0x60469778), method= "req_PQ", type = "ResPQ",
@params = Params("nonce:int128") };
schema.methods.Insert(0, addm);
static string ID(uint id) => ((int)id).ToString();
static Param[] Params(string args)
=> args.Split(' ').Select(s => { var nt = s.Split(':'); return new Param { name = nt[0], type = nt[1] }; }).ToArray();
}
void WriteTypeInfo(StreamWriter sw, TypeInfo typeInfo, string layerPrefix, bool isMethod)
{
var parentClass = typeInfo.NeedAbstract != 0 ? typeInfo.ReturnName : "ITLObject";
var genericType = typeInfo.ReturnName.Length == 1 ? $"<{typeInfo.ReturnName}>" : null;
@ -238,49 +277,185 @@ namespace WTelegram
sw.WriteLine(" }");
skipParams = typeInfo.NeedAbstract;
}
string MapName(string name) => name switch
{
"out" => "out_",
"static" => "static_",
"long" => "long_",
"default" => "default_",
"public" => "public_",
"params" => "params_",
"private" => "private_",
_ => name
};
}
string MapType(string type, string name)
private static string MapName(string name) => name switch
{
"out" => "out_",
"static" => "static_",
"long" => "long_",
"default" => "default_",
"public" => "public_",
"params" => "params_",
"private" => "private_",
_ => name
};
private string MapType(string type, string name)
{
if (type.StartsWith("Vector<", StringComparison.OrdinalIgnoreCase))
return MapType(type[7..^1], name) + "[]";
else if (type == "Bool")
return "bool";
else if (type == "bytes")
return "byte[]";
else if (type == "int128")
return "Int128";
else if (type == "int256")
return "Int256";
else if (type == "Object")
return "ITLObject";
else if (type == "!X")
return "ITLFunction<X>";
else if (typeInfos.TryGetValue(type, out var typeInfo))
return typeInfo.ReturnName;
else if (type == "int")
{
if (type.StartsWith("Vector<", StringComparison.OrdinalIgnoreCase))
return MapType(type[7..^1], name) + "[]";
else if (type == "Bool")
return "bool";
else if (type == "bytes")
return "byte[]";
else if (type == "int128")
return "Int128";
else if (type == "int256")
return "Int256";
else if (type == "Object")
return "ITLObject";
else if (type == "!X")
return "ITLFunction<X>";
else if (typeInfos.TryGetValue(type, out var typeInfo))
return typeInfo.ReturnName;
else if (type == "int")
{
var name2 = '_' + name + '_';
if (name2.EndsWith("_date_") || name2.EndsWith("_time_") || name2 == "_expires_" || name2 == "_now_" || name2.StartsWith("_valid_"))
return "DateTime";
else
return "int";
}
else if (type == "string")
return name.StartsWith("md5") ? "byte[]" : "string";
var name2 = '_' + name + '_';
if (name2.EndsWith("_date_") || name2.EndsWith("_time_") || name2.StartsWith("_valid_") ||
name2 == "_expires_" || name2 == "_expires_at_" || name2 == "_now_")
return "DateTime";
else
return type;
return "int";
}
else if (type == "string")
return name.StartsWith("md5") ? "byte[]" : "string";
else
return type;
}
private string MapOptionalType(string type, string name)
{
if (type == "Bool")
return "bool?";
else if (type == "long")
return "long?";
else if (type == "double")
return "double?";
else if (type == "int128")
return "Int128?";
else if (type == "int256")
return "Int256?";
else if (type == "int")
{
var name2 = '_' + name + '_';
if (name2.EndsWith("_date_") || name2.EndsWith("_time_") || name2 == "_expires_" || name2 == "_now_" || name2.StartsWith("_valid_"))
return "DateTime?";
else
return "int?";
}
else
return MapType(type, name);
}
private void WriteMethod(StreamWriter sw, Method method)
{
int ctorNb = int.Parse(method.id);
var funcName = CSharpName(method.method);
string returnType = MapType(method.type, "");
int style = knownStyles.GetValueOrDefault(funcName, 2);
// styles: 0 = static string, 1 = static ITLFunction<>, 2 = Task<>, -1 = skip method
if (style == -1) return;
sw.WriteLine();
sw.Write($"{tabIndent}//{method.method}#{ctorNb:x8} ");
if (method.type.Length == 1) { sw.Write($"{{{method.type}:Type}} "); funcName += $"<{returnType}>"; }
foreach (var parm in method.@params) sw.Write($"{parm.name}:{parm.type} ");
sw.WriteLine($"= {method.type}");
if (style == 0) sw.WriteLine($"{tabIndent}public Task<{returnType}> {funcName}() => CallAsync<{returnType}>({funcName});");
if (style == 0) sw.Write($"{tabIndent}public static string {funcName}(BinaryWriter writer");
if (style == 1) sw.Write($"{tabIndent}public static ITLFunction<{returnType}> {funcName}(");
if (style == 2) sw.Write($"{tabIndent}public Task<{returnType}> {funcName}(");
bool first = style != 0;
foreach (var parm in method.@params) // output non-optional parameters
{
if (parm.type == "#" || parm.type.StartsWith("flags.")) continue;
if (first) first = false; else sw.Write(", ");
sw.Write($"{MapType(parm.type, parm.name)} {MapName(parm.name)}");
}
string flagExpr = null;
foreach (var parm in method.@params) // output optional parameters
{
if (!parm.type.StartsWith("flags.")) continue;
var parmName = MapName(parm.name);
int qm = parm.type.IndexOf('?');
int bit = int.Parse(parm.type[6..qm]);
if (first) first = false; else sw.Write(", ");
if (parm.type.EndsWith("?true"))
{
sw.Write($"bool {parmName} = false");
flagExpr += $" | ({parmName} ? 0x{1 << bit:X} : 0)";
}
else
{
sw.Write($"{MapOptionalType(parm.type[(qm + 1)..], parm.name)} {parmName} = null");
flagExpr += $" | ({parmName} != null ? 0x{1 << bit:X} : 0)";
}
}
if (flagExpr != null) flagExpr = flagExpr.IndexOf('|', 3) >= 0 ? flagExpr[3..] : flagExpr[4..^1];
sw.WriteLine(")");
if (style != 0) tabIndent += "\t";
if (style == 1) sw.WriteLine($"{tabIndent}=> writer =>");
if (style == 2) sw.WriteLine($"{tabIndent}=> CallAsync<{returnType}>(writer =>");
sw.WriteLine(tabIndent + "{");
sw.WriteLine($"{tabIndent}\twriter.Write(0x{ctorNb:X8});");
foreach (var parm in method.@params) // serialize request
{
var parmName = MapName(parm.name);
var parmType = parm.type;
if (parmType.StartsWith("flags."))
{
if (parmType.EndsWith("?true")) continue;
int qm = parmType.IndexOf('?');
parmType = parmType[(qm + 1)..];
sw.WriteLine($"{tabIndent}\tif ({parmName} != null)");
sw.Write('\t');
if (MapOptionalType(parmType, parm.name).EndsWith('?'))
parmName += ".Value";
}
switch (parmType)
{
case "Bool":
sw.WriteLine($"{tabIndent}\twriter.Write({parmName} ? 0x997275B5 : 0xBC799737);");
break;
case "bytes":
sw.WriteLine($"{tabIndent}\twriter.WriteTLBytes({parmName});");
break;
case "long": case "int128": case "int256": case "double":
sw.WriteLine($"{tabIndent}\twriter.Write({parmName});");
break;
case "int":
if (MapType(parmType, parm.name) == "int")
sw.WriteLine($"{tabIndent}\twriter.Write({parmName});");
else
sw.WriteLine($"{tabIndent}\twriter.WriteTLStamp({parmName});");
break;
case "string":
if (parm.name.StartsWith("md5"))
sw.WriteLine($"{tabIndent}\twriter.WriteTLBytes({parmName});");
else
sw.WriteLine($"{tabIndent}\twriter.WriteTLString({parmName});");
break;
case "#":
sw.WriteLine($"{tabIndent}\twriter.Write({flagExpr});");
break;
case "!X":
sw.WriteLine($"{tabIndent}\t{parmName}(writer);");
break;
default:
if (parmType.StartsWith("Vector<", StringComparison.OrdinalIgnoreCase))
sw.WriteLine($"{tabIndent}\twriter.WriteTLVector({parmName});");
else
sw.WriteLine($"{tabIndent}\twriter.WriteTLObject({parmName});");
break;
}
}
sw.WriteLine($"{tabIndent}\treturn \"{funcName}\";");
if (style == 0) sw.WriteLine(tabIndent + "}");
if (style == 1) sw.WriteLine(tabIndent + "};");
if (style == 2) sw.WriteLine(tabIndent + "});");
if (style != 0) tabIndent = tabIndent[0..^1];
}
void UpdateTable(string jsonPath, string tableCs, List<TypeInfo> methods)
@ -347,8 +522,8 @@ namespace WTelegram
#pragma warning disable IDE1006 // Naming Styles
public class SchemaJson
{
public Constructor[] constructors { get; set; }
public Method[] methods { get; set; }
public List<Constructor> constructors { get; set; }
public List<Method> methods { get; set; }
}
public class Constructor

View file

@ -1,5 +1,6 @@
// This file is (mainly) generated automatically using the Generator class
using System;
using System.Threading.Tasks;
namespace TL
{
@ -12,7 +13,7 @@ namespace TL
public long[] server_public_key_fingerprints;
}
[TLDef(0x83C95AEC)] //p_q_inner_data# pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data
[TLDef(0x83C95AEC)] //p_q_inner_data#83c95aec pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data
public class PQInnerData : ITLObject
{
public byte[] pq;
@ -22,12 +23,9 @@ namespace TL
public Int128 server_nonce;
public Int256 new_nonce;
}
[TLDef(0xA9F55F95)] //p_q_inner_data_dc#a9f55f95 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data
public class PQInnerDataDC : PQInnerData
{
public int dc;
}
[TLDef(0x56FDDF88)] //p_q_inner_data_temp_dc#56fddf88 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data
[TLDef(0xA9F55F95)] //p_q_inner_data_DC#a9f55f95 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data
public class PQInnerDataDC : PQInnerData { public int dc; }
[TLDef(0x56FDDF88)] //p_q_inner_data_temp_DC#56fddf88 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data
public class PQInnerDataTempDC : PQInnerData
{
public int dc;
@ -70,11 +68,11 @@ namespace TL
public Int128 server_nonce;
public Int128 new_nonce_hashN; // 16 low order bytes from SHA1(new_nonce + (01=ok, 02=retry, 03=fail) + 8 high order bytes from SHA1(auth_key))
}
[TLDef(0x3BCBF734)] //dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer
[TLDef(0x3BCBF734)] //DH_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hashN:int128 = Set_client_DH_params_answer
public class DHGenOk : SetClientDHParamsAnswer { }
[TLDef(0x46DC1FB9)] //dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer
[TLDef(0x46DC1FB9)] //DH_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hashN:int128 = Set_client_DH_params_answer
public class DHGenRetry : SetClientDHParamsAnswer { }
[TLDef(0xA69DAE02)] //dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer
[TLDef(0xA69DAE02)] //DH_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hashN:int128 = Set_client_DH_params_answer
public class DHGenFail : SetClientDHParamsAnswer { }
[TLDef(0x75A3F765)] //bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner
@ -90,8 +88,8 @@ namespace TL
[TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult
public class RpcResult : ITLObject
{
public long req_msg_id;
public object result;
internal long req_msg_id;
internal object result;
}
[TLDef(0x2144CA19)] //rpc_error#2144ca19 error_code:int error_message:string = RpcError
@ -122,7 +120,7 @@ namespace TL
public long salt;
}
[TLDef(0xAE500895)] //future_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts
[TLDef(0xAE500895)] //future_salts#ae500895 req_msg_id:long now:int salts:Vector<FutureSalt> = FutureSalts
public class FutureSalts : ITLObject
{
public long req_msg_id;
@ -156,8 +154,8 @@ namespace TL
[TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer
public class MsgContainer : MessageContainer { public _Message[] messages; }
#pragma warning disable IDE1006 // Naming Styles
//[TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006")]
[TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message
public class _Message
{
public long msg_id;
@ -165,7 +163,6 @@ namespace TL
public int bytes;
public ITLObject body;
}
#pragma warning restore IDE1006 // Naming Styles
public abstract class MessageCopy : ITLObject { }
[TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy
@ -224,7 +221,7 @@ namespace TL
public int status;
}
[TLDef(0x7A19CB76)] //rsa_public_key n:string e:string = RSAPublicKey
[TLDef(0x7A19CB76)] //RSA_public_key#7a19cb76 n:bytes e:bytes = RSAPublicKey
public class RSAPublicKey : ITLObject
{
public byte[] n;
@ -238,61 +235,110 @@ namespace TL
public class DestroyAuthKeyNone : DestroyAuthKeyRes { }
[TLDef(0xEA109B13)] //destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes
public class DestroyAuthKeyFail : DestroyAuthKeyRes { }
}
public static partial class Fn // ---functions---
namespace WTelegram // ---functions---
{
using System.IO;
using TL;
public partial class Client
{
[TLDef(0x60469778)] //req_pq#60469778 nonce:int128 = ResPQ
public class ReqPQ : ITLFunction<ResPQ> { public Int128 nonce; }
[TLDef(0xBE7E8EF1)] //req_pq_multi#be7e8ef1 nonce:int128 = ResPQ
public class ReqPQmulti : ResPQ { }
//req_PQ#60469778 nonce:int128 = ResPQ
public Task<ResPQ> ReqPQ(Int128 nonce)
=> CallAsync<ResPQ>(writer =>
{
writer.Write(0x60469778);
writer.Write(nonce);
return "ReqPQ";
});
[TLDef(0xD712E4BE)] //req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params
public class ReqDHParams : ITLFunction<ServerDHParams>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] p;
public byte[] q;
public long public_key_fingerprint;
public byte[] encrypted_data;
}
//req_pq_multi#be7e8ef1 nonce:int128 = ResPQ
public Task<ResPQ> ReqPqMulti(Int128 nonce)
=> CallAsync<ResPQ>(writer =>
{
writer.Write(0xBE7E8EF1);
writer.Write(nonce);
return "ReqPqMulti";
});
[TLDef(0xF5045F1F)] //set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer
public class SetClientDHParams : ITLFunction<SetClientDHParamsAnswer>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] encrypted_data;
}
//req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params
public Task<ServerDHParams> ReqDHParams(Int128 nonce, Int128 server_nonce, byte[] p, byte[] q, long public_key_fingerprint, byte[] encrypted_data)
=> CallAsync<ServerDHParams>(writer =>
{
writer.Write(0xD712E4BE);
writer.Write(nonce);
writer.Write(server_nonce);
writer.WriteTLBytes(p);
writer.WriteTLBytes(q);
writer.Write(public_key_fingerprint);
writer.WriteTLBytes(encrypted_data);
return "ReqDHParams";
});
[TLDef(0x58E4A740)] //rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer
public class ReqRpcDropAnswer : ITLFunction<RpcDropAnswer> { public long req_msg_id; }
//set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer
public Task<SetClientDHParamsAnswer> SetClientDHParams(Int128 nonce, Int128 server_nonce, byte[] encrypted_data)
=> CallAsync<SetClientDHParamsAnswer>(writer =>
{
writer.Write(0xF5045F1F);
writer.Write(nonce);
writer.Write(server_nonce);
writer.WriteTLBytes(encrypted_data);
return "SetClientDHParams";
});
[TLDef(0xB921BD04)] //get_future_salts#b921bd04 num:int = FutureSalts
public class GetFutureSalts : ITLFunction<FutureSalts> { public int num; }
//rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer
public Task<RpcDropAnswer> RpcDropAnswer(long req_msg_id)
=> CallAsync<RpcDropAnswer>(writer =>
{
writer.Write(0x58E4A740);
writer.Write(req_msg_id);
return "RpcDropAnswer";
});
[TLDef(0x7ABE77EC)] //ping#7abe77ec ping_id:long = Pong
public class Ping : ITLFunction<Pong> { public long ping_id; }
//get_future_salts#b921bd04 num:int = FutureSalts
public Task<FutureSalts> GetFutureSalts(int num)
=> CallAsync<FutureSalts>(writer =>
{
writer.Write(0xB921BD04);
writer.Write(num);
return "GetFutureSalts";
});
[TLDef(0xF3427B8C)] //ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong
public class PingDelayDisconnect : ITLFunction<Pong>
{
public long ping_id;
public int disconnect_delay; // seconds
}
//ping#7abe77ec ping_id:long = Pong
public Task<Pong> Ping(long ping_id)
=> CallAsync<Pong>(writer =>
{
writer.Write(0x7ABE77EC);
writer.Write(ping_id);
return "Ping";
});
[TLDef(0xE7512126)] //destroy_session#e7512126 session_id:long = DestroySessionRes
public class DestroySession : ITLFunction<DestroySessionRes> { public long session_id; }
//ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong
public Task<Pong> PingDelayDisconnect(long ping_id, int disconnect_delay)
=> CallAsync<Pong>(writer =>
{
writer.Write(0xF3427B8C);
writer.Write(ping_id);
writer.Write(disconnect_delay);
return "PingDelayDisconnect";
});
[TLDef(0x9299359F)] //http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait
public class HttpWait : ITLObject
{
public int max_delay; // ms
public int wait_after; // ms
public int max_wait; // ms
}
//destroy_session#e7512126 session_id:long = DestroySessionRes
public Task<DestroySessionRes> DestroySession(long session_id)
=> CallAsync<DestroySessionRes>(writer =>
{
writer.Write(0xE7512126);
writer.Write(session_id);
return "DestroySession";
});
[TLDef(0xD1435160)] //destroy_auth_key#d1435160 = DestroyAuthKeyRes
public class DestroyAuthKey : ITLFunction<DestroyAuthKeyRes> { }
//destroy_auth_key#d1435160 = DestroyAuthKeyRes
public Task<DestroyAuthKeyRes> DestroyAuthKey()
=> CallAsync<DestroyAuthKeyRes>(writer =>
{
writer.Write(0xD1435160);
return "DestroyAuthKey";
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -359,5 +359,4 @@ namespace TL
[IfFlag(17)] public long grouped_id;
}
}
}

114
src/TL.cs
View file

@ -11,15 +11,15 @@ using WTelegram;
namespace TL
{
public interface ITLObject { }
public interface ITLFunction<R> : ITLObject { }
public delegate string ITLFunction<out X>(BinaryWriter writer);
public static partial class Schema
{
internal static byte[] Serialize(ITLObject msg)
internal static byte[] Serialize(this ITLObject msg)
{
using var memStream = new MemoryStream(1024);
using (var writer = new BinaryWriter(memStream))
Serialize(writer, msg);
WriteTLObject(writer, msg);
return memStream.ToArray();
}
@ -27,27 +27,15 @@ namespace TL
{
using var memStream = new MemoryStream(bytes);
using var reader = new BinaryReader(memStream);
return Deserialize<T>(reader);
return (T)reader.ReadTLObject();
}
internal static void Serialize(BinaryWriter writer, ITLObject msg)
internal static void WriteTLObject(this BinaryWriter writer, ITLObject obj)
{
var type = msg.GetType();
if (obj == null) { writer.Write(NullCtor); return; }
var type = obj.GetType();
var ctorNb = type.GetCustomAttribute<TLDefAttribute>().CtorNb;
writer.Write(ctorNb);
SerializeObject(writer, msg);
}
internal static T Deserialize<T>(BinaryReader reader) where T : ITLObject
{
var ctorNb = reader.ReadUInt32();
if (!Table.TryGetValue(ctorNb, out var realType))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
return (T)DeserializeObject(reader, realType);
}
internal static void SerializeObject(BinaryWriter writer, object obj)
{
var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
int flags = 0;
IfFlagAttribute ifFlag;
@ -56,30 +44,35 @@ namespace TL
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1 << ifFlag.Bit)) == 0) continue;
object value = field.GetValue(obj);
if (value == null)
SerializeNull(writer, field.FieldType);
writer.WriteTLNull(field.FieldType);
else
SerializeValue(writer, value);
writer.WriteTLValue(value);
if (field.Name.Equals("Flags", StringComparison.OrdinalIgnoreCase)) flags = (int)value;
}
}
internal static ITLObject DeserializeObject(BinaryReader reader, Type type)
internal static ITLObject ReadTLObject(this BinaryReader reader, Action<Type, object> notifyType = null)
{
var ctorNb = reader.ReadUInt32();
if (ctorNb == NullCtor) return null;
if (!Table.TryGetValue(ctorNb, out var type))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
var obj = Activator.CreateInstance(type);
notifyType?.Invoke(type, obj);
var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
int flags = 0;
IfFlagAttribute ifFlag;
foreach (var field in fields)
{
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1 << ifFlag.Bit)) == 0) continue;
object value = DeserializeValue(reader, field.FieldType);
object value = reader.ReadTLValue(field.FieldType);
field.SetValue(obj, value);
if (field.Name.Equals("Flags", StringComparison.OrdinalIgnoreCase)) flags = (int)value;
}
return type == typeof(GzipPacked) ? UnzipPacket((GzipPacked)obj) : (ITLObject)obj;
}
internal static void SerializeValue(BinaryWriter writer, object value)
internal static void WriteTLValue(this BinaryWriter writer, object value)
{
var type = value.GetType();
switch (Type.GetTypeCode(type))
@ -89,25 +82,21 @@ namespace TL
case TypeCode.Int64: writer.Write((long)value); break;
case TypeCode.UInt64: writer.Write((ulong)value); break;
case TypeCode.Double: writer.Write((double)value); break;
case TypeCode.String: SerializeBytes(writer, Encoding.UTF8.GetBytes((string)value)); break;
case TypeCode.DateTime: writer.Write((uint)(((DateTime)value).ToUniversalTime().Ticks / 10000000 - 62135596800L)); break;
case TypeCode.Boolean: writer.Write((bool)value ? 0x997275b5 : 0xbc799737); break;
case TypeCode.String: writer.WriteTLString((string)value); break;
case TypeCode.DateTime: writer.WriteTLStamp((DateTime)value); break;
case TypeCode.Boolean: writer.Write((bool)value ? 0x997275B5 : 0xBC799737); break;
case TypeCode.Object:
if (type.IsArray)
{
if (value is byte[] bytes)
SerializeBytes(writer, bytes);
writer.WriteTLBytes(bytes);
else
SerializeVector(writer, (Array)value);
}
writer.WriteTLVector((Array)value);
else if (value is Int128 int128)
writer.Write(int128);
else if (value is Int256 int256)
writer.Write(int256);
else if (type.IsValueType)
SerializeObject(writer, value);
else if (value is ITLObject tlObject)
Serialize(writer, tlObject);
WriteTLObject(writer, tlObject);
else
ShouldntBeHere();
break;
@ -117,7 +106,7 @@ namespace TL
}
}
internal static object DeserializeValue(BinaryReader reader, Type type)
internal static object ReadTLValue(this BinaryReader reader, Type type)
{
switch (Type.GetTypeCode(type))
{
@ -126,8 +115,8 @@ namespace TL
case TypeCode.Int64: return reader.ReadInt64();
case TypeCode.UInt64: return reader.ReadUInt64();
case TypeCode.Double: return reader.ReadDouble();
case TypeCode.String: return Encoding.UTF8.GetString(DeserializeBytes(reader));
case TypeCode.DateTime: return new DateTime((reader.ReadUInt32() + 62135596800L) * 10000000, DateTimeKind.Utc);
case TypeCode.String: return reader.ReadTLString();
case TypeCode.DateTime: return reader.ReadTLStamp();
case TypeCode.Boolean:
return reader.ReadUInt32() switch
{
@ -139,36 +128,35 @@ namespace TL
if (type.IsArray)
{
if (type == typeof(byte[]))
return DeserializeBytes(reader);
return reader.ReadTLBytes();
else if (type == typeof(_Message[]))
return DeserializeMessages(reader);
return reader.ReadTLMessages();
else
return DeserializeVector(reader, type);
return reader.ReadTLVector(type);
}
else if (type == typeof(Int128))
return new Int128(reader);
else if (type == typeof(Int256))
return new Int256(reader);
else if (type.IsValueType)
return DeserializeObject(reader, type);
else
return Deserialize<ITLObject>(reader);
return ReadTLObject(reader);
default:
ShouldntBeHere();
return null;
}
}
private static void SerializeVector(BinaryWriter writer, Array array)
internal static void WriteTLVector(this BinaryWriter writer, Array array)
{
writer.Write(VectorCtor);
if (array == null) { writer.Write(0); return; }
int count = array.Length;
writer.Write(count);
for (int i = 0; i < count; i++)
SerializeValue(writer, array.GetValue(i));
writer.WriteTLValue(array.GetValue(i));
}
private static object DeserializeVector(BinaryReader reader, Type type)
internal static Array ReadTLVector(this BinaryReader reader, Type type)
{
var ctorNb = reader.ReadInt32();
if (ctorNb != VectorCtor) throw new ApplicationException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
@ -176,12 +164,30 @@ namespace TL
int count = reader.ReadInt32();
Array array = (Array)Activator.CreateInstance(type, count);
for (int i = 0; i < count; i++)
array.SetValue(DeserializeValue(reader, elementType), i);
array.SetValue(reader.ReadTLValue(elementType), i);
return array;
}
private static void SerializeBytes(BinaryWriter writer, byte[] bytes)
internal static void WriteTLStamp(this BinaryWriter writer, DateTime datetime)
=> writer.Write((uint)(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L));
internal static DateTime ReadTLStamp(this BinaryReader reader)
=> new((reader.ReadUInt32() + 62135596800L) * 10000000, DateTimeKind.Utc);
internal static void WriteTLString(this BinaryWriter writer, string str)
{
if (str == null)
writer.Write(0);
else
writer.WriteTLBytes(Encoding.UTF8.GetBytes(str));
}
internal static string ReadTLString(this BinaryReader reader)
=> Encoding.UTF8.GetString(reader.ReadTLBytes());
internal static void WriteTLBytes(this BinaryWriter writer, byte[] bytes)
{
if (bytes == null) { writer.Write(0); return; }
int length = bytes.Length;
if (length < 254)
writer.Write((byte)length);
@ -194,7 +200,7 @@ namespace TL
while (++length % 4 != 0) writer.Write((byte)0);
}
private static byte[] DeserializeBytes(BinaryReader reader)
internal static byte[] ReadTLBytes(this BinaryReader reader)
{
byte[] bytes;
int length = reader.ReadByte();
@ -210,7 +216,7 @@ namespace TL
return bytes;
}
internal static void SerializeNull(BinaryWriter writer, Type type)
internal static void WriteTLNull(this BinaryWriter writer, Type type)
{
if (!type.IsArray)
writer.Write(NullCtor);
@ -219,7 +225,7 @@ namespace TL
writer.Write(0); // null arrays are serialized as empty
}
private static _Message[] DeserializeMessages(BinaryReader reader)
internal static _Message[] ReadTLMessages(this BinaryReader reader)
{
int count = reader.ReadInt32();
var array = new _Message[count];
@ -234,7 +240,7 @@ namespace TL
var pos = reader.BaseStream.Position;
try
{
array[i].body = (ITLObject)DeserializeValue(reader, typeof(ITLObject));
array[i].body = reader.ReadTLObject();
}
catch (Exception ex)
{
@ -245,10 +251,10 @@ namespace TL
return array;
}
private static ITLObject UnzipPacket(GzipPacked obj)
internal static ITLObject UnzipPacket(GzipPacked obj)
{
using var reader = new BinaryReader(new GZipStream(new MemoryStream(obj.packed_data), CompressionMode.Decompress));
var result = Deserialize<ITLObject>(reader);
var result = ReadTLObject(reader);
Helpers.Log(1, $" → {result.GetType().Name}");
return result;
}