2021-08-04 00:40:09 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Net;
|
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
using System.Security.Cryptography;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using TL;
|
|
|
|
|
|
using static WTelegram.Encryption;
|
|
|
|
|
|
|
|
|
|
|
|
namespace WTelegram
|
|
|
|
|
|
{
|
|
|
|
|
|
public sealed class Client : IDisposable
|
|
|
|
|
|
{
|
|
|
|
|
|
public Config TLConfig { get; private set; }
|
|
|
|
|
|
private readonly Func<string, string> _config;
|
|
|
|
|
|
private readonly Func<ITLObject, Task> _updateHandler;
|
|
|
|
|
|
private readonly int _apiId;
|
|
|
|
|
|
private readonly string _apiHash;
|
|
|
|
|
|
private readonly Session _session;
|
|
|
|
|
|
private TcpClient _tcpClient;
|
|
|
|
|
|
private NetworkStream _networkStream;
|
|
|
|
|
|
private int _frame_seqTx = 0, _frame_seqRx = 0;
|
|
|
|
|
|
private ITLObject _lastSentMsg;
|
|
|
|
|
|
private Type _lastRpcResultType = typeof(object);
|
|
|
|
|
|
private readonly List<long> _msgsToAck = new();
|
|
|
|
|
|
private int _unexpectedSaltChange;
|
|
|
|
|
|
|
|
|
|
|
|
public Client(Func<string,string> configProvider = null, Func<ITLObject, Task> updateHandler = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_config = configProvider ?? DefaultConfigOrAsk;
|
|
|
|
|
|
_updateHandler = updateHandler;
|
2021-08-04 12:03:43 +02:00
|
|
|
|
_apiId = int.Parse(Config("api_id"));
|
|
|
|
|
|
_apiHash = Config("api_hash");
|
|
|
|
|
|
_session = Session.LoadOrCreate(Config("session_pathname"));
|
2021-08-04 00:40:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2021-08-04 12:03:43 +02:00
|
|
|
|
public string Config(string config)
|
|
|
|
|
|
=> _config(config) ?? DefaultConfig(config) ?? throw new ApplicationException("You must provide a config value for " + config);
|
|
|
|
|
|
|
2021-08-04 00:40:09 +02:00
|
|
|
|
public static string DefaultConfig(string config) => config switch
|
|
|
|
|
|
{
|
|
|
|
|
|
"session_pathname" => Path.Combine(
|
|
|
|
|
|
Path.GetDirectoryName(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar))),
|
|
|
|
|
|
"WTelegram.session"),
|
|
|
|
|
|
#if DEBUG
|
|
|
|
|
|
"server_address" => "149.154.167.40:443",
|
|
|
|
|
|
#else
|
|
|
|
|
|
"server_address" => "149.154.167.50:443",
|
|
|
|
|
|
#endif
|
|
|
|
|
|
"device_model" => Environment.Is64BitOperatingSystem ? "PC 64bit" : "PC 32bit",
|
|
|
|
|
|
"system_version" => System.Runtime.InteropServices.RuntimeInformation.OSDescription,
|
|
|
|
|
|
"app_version" => Assembly.GetEntryAssembly().GetName().Version.ToString(),
|
|
|
|
|
|
"system_lang_code" => CultureInfo.InstalledUICulture.TwoLetterISOLanguageName,
|
|
|
|
|
|
"lang_pack" => "",
|
|
|
|
|
|
"lang_code" => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName,
|
2021-08-04 12:03:43 +02:00
|
|
|
|
_ => null // api_id api_hash phone_number verification_code... it's up to you to reply to these correctly
|
2021-08-04 00:40:09 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2021-08-04 12:03:43 +02:00
|
|
|
|
private static string DefaultConfigOrAsk(string config)
|
2021-08-04 00:40:09 +02:00
|
|
|
|
{
|
|
|
|
|
|
var value = DefaultConfig(config);
|
|
|
|
|
|
if (value != null) return value;
|
|
|
|
|
|
Console.Write($"Enter {config.Replace('_', ' ')}: ");
|
|
|
|
|
|
return Console.ReadLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822")]
|
|
|
|
|
|
public void LoadPublicKey(string pem) => Encryption.LoadPublicKey(pem);
|
|
|
|
|
|
|
|
|
|
|
|
public async Task ConnectAsync()
|
|
|
|
|
|
{
|
2021-08-04 12:03:43 +02:00
|
|
|
|
var endpoint = _session.DataCenter == null ? IPEndPoint.Parse(Config("server_address"))
|
2021-08-04 00:40:09 +02:00
|
|
|
|
: new IPEndPoint(IPAddress.Parse(_session.DataCenter.ip_address), _session.DataCenter.port);
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(2, $"Connecting to {endpoint}...");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
_tcpClient = new TcpClient(endpoint.AddressFamily);
|
|
|
|
|
|
await _tcpClient.ConnectAsync(endpoint.Address, endpoint.Port);
|
|
|
|
|
|
_networkStream = _tcpClient.GetStream();
|
|
|
|
|
|
_frame_seqTx = _frame_seqRx = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (_session.AuthKey == null)
|
|
|
|
|
|
await CreateAuthorizationKey(this, _session);
|
|
|
|
|
|
|
|
|
|
|
|
TLConfig = await CallAsync(new InvokeWithLayer<Config>
|
|
|
|
|
|
{
|
|
|
|
|
|
layer = Schema.Layer,
|
|
|
|
|
|
query = new InitConnection<Config>
|
|
|
|
|
|
{
|
|
|
|
|
|
api_id = _apiId,
|
2021-08-04 12:03:43 +02:00
|
|
|
|
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"),
|
2021-08-04 00:40:09 +02:00
|
|
|
|
query = new Help_GetConfig()
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task MigrateDCAsync(int dcId)
|
|
|
|
|
|
{
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(2, $"Migrate to DC {dcId}...");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
//TODO: Export/Import client authorization?
|
|
|
|
|
|
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);
|
|
|
|
|
|
if (prevFamily == AddressFamily.InterNetworkV6) // try to stay in the same connectivity
|
|
|
|
|
|
dcOptions = dcOptions.OrderByDescending(dc => dc.flags & DcOption.Flags.ipv6); // list ipv6 first
|
|
|
|
|
|
else
|
|
|
|
|
|
dcOptions = dcOptions.OrderBy(dc => dc.flags & DcOption.Flags.ipv6); // list ipv4 first
|
|
|
|
|
|
var dcOption = dcOptions.FirstOrDefault();
|
|
|
|
|
|
_session.DataCenter = dcOption ?? throw new ApplicationException($"Could not find adequate dcOption for DC {dcId}");
|
|
|
|
|
|
_session.AuthKeyID = _session.Salt = _session.Seqno = 0;
|
|
|
|
|
|
_session.AuthKey = null;
|
|
|
|
|
|
await ConnectAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
|
{
|
|
|
|
|
|
CheckMsgsToAck().Wait(1000);
|
|
|
|
|
|
_networkStream?.Dispose();
|
|
|
|
|
|
_tcpClient?.Dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task SendAsync(ITLObject msg, bool isContent = true)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_session.AuthKeyID != 0) await CheckMsgsToAck();
|
|
|
|
|
|
using var memStream = new MemoryStream(1024); //TODO: choose a useful capacity
|
|
|
|
|
|
using var writer = new BinaryWriter(memStream, Encoding.UTF8);
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
if (_session.AuthKeyID == 0) // send unencrypted message
|
|
|
|
|
|
{
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(1, $"Sending {msg.GetType().Name}...");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
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
|
|
|
|
|
|
Helpers.LittleEndian(memStream.GetBuffer(), 24, (int)memStream.Length - 28); // patch message_data_length
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(1, $"Sending {msg.GetType().Name}... (seqno {seqno})");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
//TODO: Implement MTProto 2.0
|
|
|
|
|
|
using var clearStream = new MemoryStream(1024); //TODO: choose a useful capacity
|
|
|
|
|
|
using var clearWriter = new BinaryWriter(clearStream, Encoding.UTF8);
|
|
|
|
|
|
clearWriter.Write(_session.Salt); // int64 salt
|
|
|
|
|
|
clearWriter.Write(_session.Id); // int64 session_id
|
|
|
|
|
|
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
|
|
|
|
|
|
int clearLength = (int)clearStream.Length; // length before padding (= 32 + message_data_length)
|
|
|
|
|
|
int padding = (0x7FFFFFF0 - clearLength) % 16;
|
|
|
|
|
|
clearStream.SetLength(clearLength + padding);
|
|
|
|
|
|
byte[] clearBuffer = clearStream.GetBuffer();
|
|
|
|
|
|
Helpers.LittleEndian(clearBuffer, 28, clearLength - 32); // patch message_data_length
|
|
|
|
|
|
RNG.GetBytes(clearBuffer, clearLength, padding);
|
|
|
|
|
|
var clearSha1 = SHA1.HashData(clearBuffer.AsSpan(0, clearLength)); // padding excluded from computation!
|
|
|
|
|
|
byte[] encrypted_data = EncryptDecryptMessage(clearBuffer.AsSpan(0, clearLength + padding), true, _session.AuthKey, clearSha1);
|
|
|
|
|
|
|
|
|
|
|
|
writer.Write(_session.AuthKeyID); // int64 auth_key_id
|
|
|
|
|
|
writer.Write(clearSha1, 4, 16); // int128 msg_key = low 128-bits of SHA1(clear message body)
|
|
|
|
|
|
writer.Write(encrypted_data); // bytes encrypted_data
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var buffer = memStream.GetBuffer();
|
|
|
|
|
|
int frameLength = (int)memStream.Length;
|
|
|
|
|
|
//TODO: support Quick Ack?
|
|
|
|
|
|
Helpers.LittleEndian(buffer, 0, frameLength + 4); // patch frame_len with correct value
|
|
|
|
|
|
uint crc = Force.Crc32.Crc32Algorithm.Compute(buffer, 0, frameLength);
|
|
|
|
|
|
writer.Write(crc); // int32 frame_crc
|
|
|
|
|
|
var frame = memStream.GetBuffer().AsMemory(0, frameLength + 4);
|
|
|
|
|
|
//TODO: support Transport obfuscation?
|
|
|
|
|
|
|
|
|
|
|
|
await _networkStream.WriteAsync(frame);
|
|
|
|
|
|
_lastSentMsg = msg;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-08-04 10:11:07 +02:00
|
|
|
|
internal async Task<ITLObject> RecvInternalAsync()
|
2021-08-04 00:40:09 +02:00
|
|
|
|
{
|
|
|
|
|
|
var data = await RecvFrameAsync();
|
|
|
|
|
|
if (data.Length == 4 && data[3] == 0xFF)
|
|
|
|
|
|
throw new ApplicationException($"Server replied with error code: {TransportError(-BitConverter.ToInt32(data))}");
|
|
|
|
|
|
if (data.Length < 24) // authKeyId+msgId+length+ctorNb | authKeyId+msgKey
|
|
|
|
|
|
throw new ApplicationException($"Packet payload too small: {data.Length}");
|
|
|
|
|
|
|
|
|
|
|
|
//TODO: ignore msgId <= lastRecvMsgId, ignore MsgId >= 30 sec in the future or < 300 sec in the past
|
|
|
|
|
|
long authKeyId = BitConverter.ToInt64(data);
|
|
|
|
|
|
if (authKeyId == 0) // Unencrypted message
|
|
|
|
|
|
{
|
|
|
|
|
|
using var reader = new BinaryReader(new MemoryStream(data, 8, data.Length - 8));
|
|
|
|
|
|
long msgId = reader.ReadInt64();
|
|
|
|
|
|
if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}");
|
|
|
|
|
|
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.Mappings.TryGetValue(ctorNb, out var realType))
|
|
|
|
|
|
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(1, $"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} unencrypted");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
return Schema.DeserializeObject(reader, realType);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (authKeyId != _session.AuthKeyID)
|
|
|
|
|
|
throw new ApplicationException($"Received a packet encrypted with unexpected key {authKeyId:X}");
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
byte[] decrypted_data = EncryptDecryptMessage(data.AsSpan(24), false, _session.AuthKey, data[4..24]);
|
|
|
|
|
|
if (decrypted_data.Length < 36) // header below+ctorNb
|
|
|
|
|
|
throw new ApplicationException($"Decrypted packet too small: {decrypted_data.Length}");
|
|
|
|
|
|
using var reader = new BinaryReader(new MemoryStream(decrypted_data));
|
|
|
|
|
|
var serverSalt = reader.ReadInt64(); // int64 salt
|
|
|
|
|
|
var sessionId = reader.ReadInt64(); // int64 session_id
|
|
|
|
|
|
var msgId = reader.ReadInt64(); // int64 message_id
|
|
|
|
|
|
var seqno = reader.ReadInt32(); // int32 msg_seqno
|
|
|
|
|
|
var length = reader.ReadInt32(); // int32 message_data_length
|
|
|
|
|
|
|
|
|
|
|
|
if (serverSalt != _session.Salt)
|
|
|
|
|
|
if (++_unexpectedSaltChange >= 10)
|
|
|
|
|
|
throw new ApplicationException($"Server salt changed unexpectedly more than 10 times during this run");
|
|
|
|
|
|
if (sessionId != _session.Id) throw new ApplicationException($"Unexpected session ID {_session.Id} != {_session.Id}");
|
|
|
|
|
|
if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}");
|
|
|
|
|
|
if ((seqno & 1) != 0) lock(_msgsToAck) _msgsToAck.Add(msgId);
|
|
|
|
|
|
if (decrypted_data.Length - 32 - length is < 0 or > 15) throw new ApplicationException($"Unexpected decrypted message_data_length {length} / {decrypted_data.Length - 32}");
|
|
|
|
|
|
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.Mappings.TryGetValue(ctorNb, out var realType))
|
|
|
|
|
|
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(1, $"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} {(seqno == -1 ? "clearText" : "isContent")}={(seqno & 1) != 0}");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
if (realType == typeof(RpcResult))
|
|
|
|
|
|
return DeserializeRpcResult(reader); // hack necessary because some RPC return bare types like bool or int[]
|
|
|
|
|
|
else
|
|
|
|
|
|
return Schema.DeserializeObject(reader, realType);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static string TransportError(int error_code) => error_code switch
|
|
|
|
|
|
{
|
|
|
|
|
|
404 => "Auth key not found",
|
|
|
|
|
|
429 => "Transport flood",
|
|
|
|
|
|
_ => ((HttpStatusCode)error_code).ToString(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task<byte[]> RecvFrameAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
byte[] frame = new byte[8];
|
|
|
|
|
|
await FullReadAsync(frame, 8);
|
|
|
|
|
|
int length = BitConverter.ToInt32(frame) - 12;
|
|
|
|
|
|
if (length <= 0 || length >= 0x10000)
|
|
|
|
|
|
throw new ApplicationException("Invalid frame_len");
|
|
|
|
|
|
int seqno = BitConverter.ToInt32(frame, 4);
|
|
|
|
|
|
if (seqno != _frame_seqRx++)
|
|
|
|
|
|
{
|
|
|
|
|
|
Trace.TraceWarning($"Unexpected frame_seq received: {seqno} instead of {_frame_seqRx}");
|
|
|
|
|
|
_frame_seqRx = seqno + 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
var payload = new byte[length];
|
|
|
|
|
|
await FullReadAsync(payload, length);
|
|
|
|
|
|
uint crc32 = Force.Crc32.Crc32Algorithm.Compute(frame, 0, 8);
|
|
|
|
|
|
crc32 = Force.Crc32.Crc32Algorithm.Append(crc32, payload);
|
|
|
|
|
|
await FullReadAsync(frame, 4);
|
|
|
|
|
|
if (crc32 != BitConverter.ToUInt32(frame))
|
|
|
|
|
|
throw new ApplicationException("Invalid envelope CRC32");
|
|
|
|
|
|
return payload;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task FullReadAsync(byte[] buffer, int length)
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int offset = 0; offset != length;)
|
|
|
|
|
|
{
|
|
|
|
|
|
var read = await _networkStream.ReadAsync(buffer.AsMemory(offset, length - offset));
|
|
|
|
|
|
if (read == 0) throw new ApplicationException("Connection shut down");
|
|
|
|
|
|
offset += read;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-08-04 10:11:07 +02:00
|
|
|
|
private RpcResult DeserializeRpcResult(BinaryReader reader)
|
2021-08-04 00:40:09 +02:00
|
|
|
|
{
|
|
|
|
|
|
long reqMsgId = reader.ReadInt64();
|
|
|
|
|
|
var rpcResult = new RpcResult { req_msg_id = reqMsgId };
|
|
|
|
|
|
if (reqMsgId == _session.LastSentMsgId)
|
|
|
|
|
|
rpcResult.result = Schema.DeserializeValue(reader, _lastRpcResultType);
|
|
|
|
|
|
else
|
2021-08-04 10:11:07 +02:00
|
|
|
|
rpcResult.result = Schema.Deserialize<ITLObject>(reader);
|
2021-08-04 00:40:09 +02:00
|
|
|
|
return rpcResult;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public class RpcException : Exception
|
|
|
|
|
|
{
|
|
|
|
|
|
public readonly int Code;
|
|
|
|
|
|
public RpcException(int code, string message) : base(message) => Code = code;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<X> CallAsync<X>(ITLFunction<X> request)
|
|
|
|
|
|
{
|
|
|
|
|
|
await SendAsync(request);
|
|
|
|
|
|
_lastRpcResultType = typeof(X);
|
|
|
|
|
|
for (; ;)
|
|
|
|
|
|
{
|
|
|
|
|
|
var reply = await RecvInternalAsync();
|
|
|
|
|
|
if (reply is RpcResult rpcResult && rpcResult.req_msg_id == _session.LastSentMsgId)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (rpcResult.result is RpcError rpcError)
|
|
|
|
|
|
{
|
|
|
|
|
|
int migrateDC;
|
|
|
|
|
|
if (rpcError.error_code == 303 && ((migrateDC = rpcError.error_message.IndexOf("_MIGRATE_")) > 0))
|
|
|
|
|
|
{
|
|
|
|
|
|
migrateDC = int.Parse(rpcError.error_message[(migrateDC + 9)..]);
|
|
|
|
|
|
await MigrateDCAsync(migrateDC);
|
|
|
|
|
|
await SendAsync(request);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
throw new RpcException(rpcError.error_code, rpcError.error_message);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (rpcResult.result is X result)
|
|
|
|
|
|
return result;
|
|
|
|
|
|
else
|
|
|
|
|
|
throw new ApplicationException($"{request.GetType().Name} call got a result of type {rpcResult.result.GetType().Name} instead of {typeof(X).Name}");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
await HandleMessageAsync(reply);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async Task CheckMsgsToAck()
|
|
|
|
|
|
{
|
|
|
|
|
|
MsgsAck msgsAck = null;
|
|
|
|
|
|
lock (_msgsToAck)
|
|
|
|
|
|
if (_msgsToAck.Count != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
msgsAck = new MsgsAck { msg_ids = _msgsToAck.ToArray() };
|
|
|
|
|
|
_msgsToAck.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (msgsAck != null)
|
|
|
|
|
|
await SendAsync(msgsAck, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-08-04 10:11:07 +02:00
|
|
|
|
private async Task HandleMessageAsync(ITLObject obj)
|
2021-08-04 00:40:09 +02:00
|
|
|
|
{
|
|
|
|
|
|
switch (obj)
|
|
|
|
|
|
{
|
|
|
|
|
|
case MsgContainer container:
|
|
|
|
|
|
foreach (var msg in container.messages)
|
|
|
|
|
|
{
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(1, $" → {msg.body?.GetType().Name,-48} timestamp={_session.MsgIdToStamp(msg.msg_id)} isResponse={(msg.msg_id & 2) != 0} {(msg.seqno == -1 ? "clearText" : "isContent")}={(msg.seqno & 1) != 0}");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
if ((msg.seqno & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msg.msg_id);
|
|
|
|
|
|
if (msg.body != null) await HandleMessageAsync(msg.body);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case BadServerSalt badServerSalt:
|
|
|
|
|
|
_session.Salt = badServerSalt.new_server_salt;
|
|
|
|
|
|
if (badServerSalt.bad_msg_id == _session.LastSentMsgId)
|
|
|
|
|
|
await SendAsync(_lastSentMsg);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case BadMsgNotification badMsgNotification:
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(3, $"BadMsgNotification {badMsgNotification.error_code} for msg {badMsgNotification.bad_msg_seqno}");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
break;
|
|
|
|
|
|
case RpcResult rpcResult:
|
|
|
|
|
|
if (_session.MsgIdToStamp(rpcResult.req_msg_id) >= _session.SessionStart)
|
|
|
|
|
|
throw new ApplicationException($"Got RpcResult({rpcResult.result.GetType().Name}) for unknown msgId {rpcResult.req_msg_id}");
|
|
|
|
|
|
break; // silently ignore results for msg_id from previous sessions
|
|
|
|
|
|
default:
|
2021-08-04 10:11:07 +02:00
|
|
|
|
_updateHandler?.Invoke(obj);
|
2021-08-04 00:40:09 +02:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task<User> UserAuthIfNeeded(CodeSettings settings = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_session.User != null)
|
|
|
|
|
|
return Schema.Deserialize<User>(_session.User);
|
2021-08-04 12:03:43 +02:00
|
|
|
|
string phone_number = Config("phone_number");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
var sentCode = await CallAsync(new Auth_SendCode
|
|
|
|
|
|
{
|
|
|
|
|
|
phone_number = phone_number,
|
|
|
|
|
|
api_id = _apiId,
|
|
|
|
|
|
api_hash = _apiHash,
|
|
|
|
|
|
settings = settings ?? new()
|
|
|
|
|
|
});
|
2021-08-04 10:11:07 +02:00
|
|
|
|
Helpers.Log(3, $"A verification code has been sent via {sentCode.type.GetType().Name[17..]}");
|
2021-08-04 12:03:43 +02:00
|
|
|
|
var verification_code = Config("verification_code");
|
2021-08-04 00:40:09 +02:00
|
|
|
|
var authorization = await CallAsync(new Auth_SignIn
|
|
|
|
|
|
{
|
|
|
|
|
|
phone_number = phone_number,
|
|
|
|
|
|
phone_code_hash = sentCode.phone_code_hash,
|
|
|
|
|
|
phone_code = verification_code
|
|
|
|
|
|
});
|
|
|
|
|
|
if (authorization is not Auth_Authorization { user: User user } auth_success)
|
|
|
|
|
|
throw new ApplicationException("Failed to get Authorization: " + authorization.GetType().Name);
|
|
|
|
|
|
//TODO: support Auth_AuthorizationSignUpRequired?
|
|
|
|
|
|
_session.User = Schema.Serialize(user);
|
|
|
|
|
|
return user;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|