using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using TL;
using static WTelegram.Encryption;
// necessary for .NET Standard 2.0 compilation:
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
namespace WTelegram
{
public partial class Client : IDisposable
{
/// This event will be called when unsollicited updates/messages are sent by Telegram servers
/// Make your handler , or return or See Examples/Program_ListenUpdate.cs for how to use this
public event Func OnUpdate;
/// This event is called for other types of notifications (login states, reactor errors, ...)
public event Func OnOther;
/// Use this handler to intercept Updates that resulted from your own API calls
public event Func OnOwnUpdate;
/// Used to create a TcpClient connected to the given address/port, or throw an exception on failure
public TcpFactory TcpHandler { get; set; } = DefaultTcpHandler;
public delegate Task TcpFactory(string host, int port);
/// Url for using a MTProxy. https://t.me/proxy?server=...
public string MTProxyUrl { get; set; }
/// Telegram configuration, obtained at connection time
public Config TLConfig { get; private set; }
/// Number of automatic reconnections on connection/reactor failure
public int MaxAutoReconnects { get; set; } = 5;
/// Number of attempts in case of wrong verification_code or password
public int MaxCodePwdAttempts { get; set; } = 3;
/// Number of seconds under which an error 420 FLOOD_WAIT_X will not be raised and your request will instead be auto-retried after the delay
public int FloodRetryThreshold { get; set; } = 60;
/// Number of seconds between each keep-alive ping. Increase this if you have a slow connection or you're debugging your code
public int PingInterval { get; set; } = 60;
/// Size of chunks when uploading/downloading files. Reduce this if you don't have much memory
public int FilePartSize { get; set; } = 512 * 1024;
/// Is this Client instance the main or a secondary DC session
public bool IsMainDC => (_dcSession?.DataCenter?.id - _session.MainDC) is null or 0;
/// Has this Client established connection been disconnected?
public bool Disconnected => _tcpClient != null && !(_tcpClient.Client?.Connected ?? false);
/// ID of the current logged-in user or 0
public long UserId => _session.UserId;
/// Info about the current logged-in user. This is only filled after a successful (re)login, not updated later
public User User { get; private set; }
private Func _config;
private readonly Session _session;
private string _apiHash;
private Session.DCSession _dcSession;
private TcpClient _tcpClient;
private Stream _networkStream;
private IObject _lastSentMsg;
private long _lastRecvMsgId;
private readonly List _msgsToAck = [];
private readonly Random _random = new();
private int _saltChangeCounter;
private Task _reactorTask;
private Rpc _bareRpc;
private readonly Dictionary _pendingRpcs = [];
private SemaphoreSlim _sendSemaphore = new(0);
private readonly SemaphoreSlim _semaphore = new(1);
private Task _connecting;
private CancellationTokenSource _cts;
private int _reactorReconnects = 0;
private const string ConnectionShutDown = "Could not read payload length : Connection shut down";
private const long Ticks5Secs = 5 * TimeSpan.TicksPerSecond;
private readonly SemaphoreSlim _parallelTransfers = new(10); // max parallel part uploads/downloads
private readonly SHA256 _sha256 = SHA256.Create();
private readonly SHA256 _sha256Recv = SHA256.Create();
#if OBFUSCATION
private AesCtr _sendCtr, _recvCtr;
#endif
private bool _paddedMode;
public Client(int apiID, string apiHash, string sessionPathname = null)
: this(what => what switch
{
"api_id" => apiID.ToString(),
"api_hash" => apiHash,
"session_pathname" => sessionPathname,
_ => null
})
{ }
public Client(Func configProvider, byte[] startSession, Action saveSession)
: this(configProvider, new ActionStore(startSession, saveSession)) { }
/// Welcome to WTelegramClient! 🙂
/// Config callback, is queried for: api_id, api_hash, session_pathname
/// if specified, must support initial Length & Read() of a session, then calls to Write() the updated session. Other calls can be ignored
public Client(Func configProvider = null, Stream sessionStore = null)
{
_config = configProvider ?? DefaultConfigOrAsk;
var session_key = _config("session_key") ?? (_apiHash = Config("api_hash"));
sessionStore ??= new SessionStore(Config("session_pathname"));
_session = Session.LoadOrCreate(sessionStore, Convert.FromHexString(session_key));
if (_session.ApiId == 0) _session.ApiId = int.Parse(Config("api_id"));
if (_session.MainDC != 0) _session.DCSessions.TryGetValue(_session.MainDC, out _dcSession);
_dcSession ??= new() { Id = Helpers.RandomLong() };
_dcSession.Client = this;
var version = Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion;
Helpers.Log(1, $"WTelegramClient {version} running under {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}");
}
private Client(Client cloneOf, Session.DCSession dcSession)
{
_config = cloneOf._config;
_session = cloneOf._session;
TcpHandler = cloneOf.TcpHandler;
MTProxyUrl = cloneOf.MTProxyUrl;
PingInterval = cloneOf.PingInterval;
TLConfig = cloneOf.TLConfig;
_dcSession = dcSession;
}
internal Task ConfigAsync(string what) => Task.Run(() => Config(what));
internal string Config(string what)
=> _config(what) ?? DefaultConfig(what) ?? throw new WTException("You must provide a config value for " + what);
/// Default config values, used if your Config callback returns
public static string DefaultConfig(string what) => what switch
{
"session_pathname" => Path.Combine(
Path.GetDirectoryName(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)))
?? AppDomain.CurrentDomain.BaseDirectory, "WTelegram.session"),
#if DEBUG
"server_address" => "2>149.154.167.40:443", // Test DC 2
#else
"server_address" => "2>149.154.167.50:443", // DC 2
#endif
"device_model" => Environment.Is64BitOperatingSystem ? "PC 64bit" : "PC 32bit",
"system_version" => Helpers.GetSystemVersion(),
"app_version" => Helpers.GetAppVersion(),
"system_lang_code" => CultureInfo.InstalledUICulture.TwoLetterISOLanguageName,
"lang_pack" => "",
"lang_code" => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName,
"user_id" => "-1",
"verification_code" or "email_verification_code" or "password" => AskConfig(what),
"init_params" => "{}",
_ => null // api_id api_hash phone_number... it's up to you to reply to these correctly
};
internal static string DefaultConfigOrAsk(string config) => DefaultConfig(config) ?? AskConfig(config);
private static string AskConfig(string config)
{
if (config == "session_key")
{
Console.WriteLine("Welcome! You can obtain your api_id/api_hash at https://my.telegram.org/apps");
return null;
}
Console.Write($"Enter {config.Replace('_', ' ')}: ");
return Console.ReadLine();
}
/// Load a specific Telegram server public key
/// A string starting with -----BEGIN RSA PUBLIC KEY-----
public static void LoadPublicKey(string pem) => Encryption.LoadPublicKey(pem);
/// Builds a structure that is used to validate a 2FA password
/// Password validation configuration. You can obtain this via Account_GetPassword or through OnOther as part of the login process
/// The password to validate
public static Task InputCheckPassword(Account_Password accountPassword, string password)
=> Check2FA(accountPassword, () => Task.FromResult(password));
public void Dispose()
{
Helpers.Log(2, $"{_dcSession.DcID}>Disposing the client");
Reset(false, IsMainDC);
var ex = new TaskCanceledException("WTelegram.Client was disposed");
lock (_pendingRpcs) // abort all pending requests
foreach (var rpc in _pendingRpcs.Values)
rpc.tcs.TrySetException(ex);
_networkStream = null;
if (IsMainDC) _session.Dispose();
GC.SuppressFinalize(this);
}
public void DisableUpdates(bool disable = true) => _dcSession.DisableUpdates(disable);
/// Disconnect from Telegram (shouldn't be needed in normal usage)
/// Forget about logged-in user
/// Disconnect secondary sessions with other DCs
public void Reset(bool resetUser = true, bool resetSessions = true)
{
try
{
if (CheckMsgsToAck() is MsgsAck msgsAck)
SendAsync(msgsAck, false).Wait(1000);
}
catch { }
_cts?.Cancel();
_sendSemaphore = new(0); // initially taken, first released during DoConnectAsync
try
{
_reactorTask?.Wait(1000);
}
catch { }
_reactorTask = resetSessions ? null : Task.CompletedTask;
_networkStream?.Close();
_tcpClient?.Dispose();
#if OBFUSCATION
_sendCtr?.Dispose();
_recvCtr?.Dispose();
#endif
_paddedMode = false;
_connecting = null;
_bareRpc = null;
if (resetSessions)
{
foreach (var altSession in _session.DCSessions.Values)
if (altSession.Client != null && altSession.Client != this)
{
altSession.Client.Dispose();
altSession.Client = null;
}
}
if (resetUser)
{
_loginCfg = default;
_session.UserId = 0;
User = null;
}
}
private Session.DCSession GetOrCreateDCSession(int dcId, DcOption.Flags flags)
{
if (_session.DCSessions.TryGetValue(dcId, out var dcSession))
if (dcSession.Client != null || dcSession.DataCenter.flags == flags)
return dcSession; // if we have already a session with this DC and we are connected or it is a perfect match, use it
// try to find the most appropriate DcOption for this DC
if ((dcSession?.AuthKeyID ?? 0) == 0) // we will need to negociate an AuthKey => can't use media_only DC
flags &= ~DcOption.Flags.media_only;
var dcOptions = _session.DcOptions.Where(dc => dc.id == dcId).OrderBy(dc => dc.flags ^ flags);
var dcOption = dcOptions.FirstOrDefault() ?? throw new WTException($"Could not find adequate dc_option for DC {dcId}");
dcSession ??= new Session.DCSession { Id = Helpers.RandomLong() }; // create new session only if not already existing
dcSession.DataCenter = dcOption;
return _session.DCSessions[dcId] = dcSession;
}
/// Obtain/create a Client for a secondary session on a specific Data Center
/// ID of the Data Center
/// Session will be used only for transferring media
/// Connect immediately
/// Client connected to the selected DC
public async Task GetClientForDC(int dcId, bool media_only = true, bool connect = true)
{
if (_dcSession.DataCenter?.id == dcId) return this;
Session.DCSession altSession;
lock (_session)
{
altSession = GetOrCreateDCSession(dcId, _dcSession.DataCenter.flags | (media_only ? DcOption.Flags.media_only : 0));
if (altSession.Client?.Disconnected ?? false) { altSession.Client.Dispose(); altSession.Client = null; }
altSession.Client ??= new Client(this, altSession);
}
Helpers.Log(2, $"Requested connection to DC {dcId}...");
if (connect)
{
await _semaphore.WaitAsync();
try
{
Auth_ExportedAuthorization exported = null;
if (_session.UserId != 0 && IsMainDC && altSession.UserId != _session.UserId)
exported = await this.Auth_ExportAuthorization(dcId);
await altSession.Client.ConnectAsync();
if (exported != null)
{
var authorization = await altSession.Client.Auth_ImportAuthorization(exported.id, exported.bytes);
if (authorization is not Auth_Authorization { user: User user })
throw new WTException("Failed to get Authorization: " + authorization.GetType().Name);
altSession.UserId = user.id;
}
}
finally
{
_semaphore.Release();
}
}
return altSession.Client;
}
private async Task Reactor(Stream stream, CancellationTokenSource cts)
{
const int MinBufferSize = 1024;
var data = new byte[MinBufferSize];
while (!cts.IsCancellationRequested)
{
IObject obj = null;
try
{
if (await stream.FullReadAsync(data, 4, cts.Token) != 4)
throw new WTException(ConnectionShutDown);
#if OBFUSCATION
_recvCtr.EncryptDecrypt(data, 4);
#endif
int payloadLen = BinaryPrimitives.ReadInt32LittleEndian(data);
if (payloadLen <= 0)
throw new WTException("Could not read frame data : Invalid payload length");
else if (payloadLen > data.Length)
data = new byte[payloadLen];
else if (Math.Max(payloadLen, MinBufferSize) < data.Length / 4)
data = new byte[Math.Max(payloadLen, MinBufferSize)];
if (await stream.FullReadAsync(data, payloadLen, cts.Token) != payloadLen)
throw new WTException("Could not read frame data : Connection shut down");
#if OBFUSCATION
_recvCtr.EncryptDecrypt(data, payloadLen);
#endif
obj = ReadFrame(data, payloadLen);
}
catch (Exception ex) // an exception in RecvAsync is always fatal
{
if (cts.IsCancellationRequested) return;
bool disconnectedAltDC = !IsMainDC && ex is WTException { Message: ConnectionShutDown } or IOException { InnerException: SocketException };
if (disconnectedAltDC)
Helpers.Log(3, $"{_dcSession.DcID}>Alt DC disconnected: {ex.Message}");
else
Helpers.Log(5, $"{_dcSession.DcID}>An exception occured in the reactor: {ex}");
var oldSemaphore = _sendSemaphore;
await oldSemaphore.WaitAsync(cts.Token); // prevent any sending while we reconnect
var reactorError = new ReactorError { Exception = ex };
try
{
lock (_msgsToAck) _msgsToAck.Clear();
Reset(false, false);
_reactorReconnects = (_reactorReconnects + 1) % MaxAutoReconnects;
if (disconnectedAltDC && _pendingRpcs.Count <= 1)
if (_pendingRpcs.Values.FirstOrDefault() is not Rpc rpc || rpc.type == typeof(Pong))
_reactorReconnects = 0;
if (_reactorReconnects == 0)
throw;
await Task.Delay(5000);
if (_networkStream == null) return; // Dispose has been called in-between
await ConnectAsync(); // start a new reactor after 5 secs
lock (_pendingRpcs) // retry all pending requests
{
foreach (var rpc in _pendingRpcs.Values)
rpc.tcs.SetResult(reactorError); // this leads to a retry (see Invoke method)
_pendingRpcs.Clear();
_bareRpc = null;
}
// TODO: implement an Updates gaps handling system? https://core.telegram.org/api/updates
if (IsMainDC)
{
var updatesState = await this.Updates_GetState(); // this call reenables incoming Updates
RaiseUpdate(updatesState);
}
}
catch
{
if (IsMainDC)
RaiseUpdate(reactorError);
lock (_pendingRpcs) // abort all pending requests
{
foreach (var rpc in _pendingRpcs.Values)
rpc.tcs.SetException(ex);
_pendingRpcs.Clear();
_bareRpc = null;
}
}
finally
{
oldSemaphore.Release();
}
}
if (obj != null)
await HandleMessageAsync(obj);
}
}
internal DateTime MsgIdToStamp(long serverMsgId)
=> new((serverMsgId >> 32) * 10000000 - _dcSession.ServerTicksOffset + 621355968000000000L, DateTimeKind.Utc);
internal IObject ReadFrame(byte[] data, int dataLen)
{
if (dataLen < 8 && data[3] == 0xFF)
{
int error_code = -BinaryPrimitives.ReadInt32LittleEndian(data);
throw new RpcException(error_code, TransportError(error_code));
}
if (dataLen < 24) // authKeyId+msgId+length+ctorNb | authKeyId+msgKey
throw new WTException($"Packet payload too small: {dataLen}");
long authKeyId = BinaryPrimitives.ReadInt64LittleEndian(data);
if (authKeyId != _dcSession.AuthKeyID)
throw new WTException($"Received a packet encrypted with unexpected key {authKeyId:X}");
if (authKeyId == 0) // Unencrypted message
{
using var reader = new BinaryReader(new MemoryStream(data, 8, dataLen - 8));
long msgId = _lastRecvMsgId = reader.ReadInt64();
if ((msgId & 1) == 0) throw new WTException($"Invalid server msgId {msgId}");
int length = reader.ReadInt32();
dataLen -= 20;
if (length > dataLen || dataLen - length > (_paddedMode ? 256 : 0))
throw new WTException($"Unexpected unencrypted/padding length {dataLen} - {length}");
var obj = reader.ReadTLObject();
Helpers.Log(1, $"{_dcSession.DcID}>Receiving {obj.GetType().Name,-40} {MsgIdToStamp(msgId):u} clear{((msgId & 2) == 0 ? "" : " NAR")}");
if (_bareRpc == null) throw new WTException("Shouldn't receive unencrypted packet at this point");
return obj;
}
else
{
byte[] decrypted_data = EncryptDecryptMessage(data.AsSpan(24, (dataLen - 24) & ~0xF), false, 8, _dcSession.AuthKey, data, 8, _sha256Recv);
if (decrypted_data.Length < 36) // header below+ctorNb
throw new WTException($"Decrypted packet too small: {decrypted_data.Length}");
_sha256Recv.TransformBlock(_dcSession.AuthKey, 96, 32, null, 0);
_sha256Recv.TransformFinalBlock(decrypted_data, 0, decrypted_data.Length);
if (!data.AsSpan(8, 16).SequenceEqual(_sha256Recv.Hash.AsSpan(8, 16)))
throw new WTException("Mismatch between MsgKey & decrypted SHA256");
_sha256Recv.Initialize();
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 (length < 0 || length % 4 != 0) throw new WTException($"Invalid message_data_length: {length}");
if (decrypted_data.Length - 32 - length is < 12 or > 1024) throw new WTException($"Invalid message padding length: {decrypted_data.Length - 32}-{length}");
if (sessionId != _dcSession.Id) throw new WTException($"Unexpected session ID: {sessionId} != {_dcSession.Id}");
if ((msgId & 1) == 0) throw new WTException($"msg_id is not odd: {msgId}");
if (!_dcSession.CheckNewMsgId(msgId))
{
Helpers.Log(3, $"{_dcSession.DcID}>Ignoring duplicate or old msg_id {msgId}");
return null;
}
var utcNow = DateTime.UtcNow;
if (_lastRecvMsgId == 0) // resync ServerTicksOffset on first message
_dcSession.ServerTicksOffset = (msgId >> 32) * 10000000 - utcNow.Ticks + 621355968000000000L;
var msgStamp = MsgIdToStamp(_lastRecvMsgId = msgId);
long deltaTicks = (msgStamp - utcNow).Ticks;
if (deltaTicks is > 0)
if (deltaTicks < Ticks5Secs) // resync if next message is less than 5 seconds in the future
_dcSession.ServerTicksOffset += deltaTicks;
else if (_dcSession.ServerTicksOffset < -Ticks5Secs && deltaTicks + _dcSession.ServerTicksOffset < 0)
_dcSession.ServerTicksOffset += deltaTicks;
if (serverSalt != _dcSession.Salt && serverSalt != _dcSession.OldSalt && serverSalt != _dcSession.Salts?.Values.ElementAtOrDefault(1))
{
Helpers.Log(3, $"{_dcSession.DcID}>Server salt has changed: {_dcSession.Salt:X} -> {serverSalt:X}");
_dcSession.OldSalt = _dcSession.Salt;
_dcSession.Salt = serverSalt;
if (++_saltChangeCounter >= 10)
throw new WTException("Server salt changed too often! Security issue?");
CheckSalt();
}
if ((seqno & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msgId);
var ctorNb = reader.ReadUInt32();
if (ctorNb != Layer.BadMsgCtor && deltaTicks / TimeSpan.TicksPerSecond is > 30 or < -300)
{ // msg_id values that belong over 30 seconds in the future or over 300 seconds in the past are to be ignored.
Helpers.Log(1, $"{_dcSession.DcID}>Ignoring 0x{ctorNb:X8} because of wrong timestamp {msgStamp:u} - {utcNow:u} Δ={new TimeSpan(_dcSession.ServerTicksOffset):c}");
return null;
}
if (ctorNb == Layer.MsgContainerCtor)
{
Helpers.Log(1, $"{_dcSession.DcID}>Receiving {"MsgContainer",-40} {msgStamp:u} (svc)");
return ReadMsgContainer(reader);
}
else if (ctorNb == Layer.RpcResultCtor)
{
Helpers.Log(1, $"{_dcSession.DcID}>Receiving {"RpcResult",-40} {msgStamp:u}");
return ReadRpcResult(reader);
}
else
{
var obj = reader.ReadTLObject(ctorNb);
Helpers.Log(1, $"{_dcSession.DcID}>Receiving {obj.GetType().Name,-40} {msgStamp:u} {((seqno & 1) != 0 ? "" : "(svc)")} {((msgId & 2) == 0 ? "" : "NAR")}");
return obj;
}
}
static string TransportError(int error_code) => error_code switch
{
404 => "Auth key not found",
429 => "Transport flood",
444 => "Invalid DC",
_ => Enum.GetName(typeof(HttpStatusCode), error_code) ?? "Transport error"
};
}
internal void CheckSalt()
{
lock (_session)
{
_dcSession.Salts ??= [];
if (_dcSession.Salts.Count != 0)
{
var keys = _dcSession.Salts.Keys;
if (keys[^1] == DateTime.MaxValue) return; // GetFutureSalts ongoing
var now = DateTime.UtcNow.AddTicks(_dcSession.ServerTicksOffset);
for (; keys.Count > 1 && keys[1] < now; _dcSession.OldSalt = _dcSession.Salt, _dcSession.Salt = _dcSession.Salts.Values[0])
_dcSession.Salts.RemoveAt(0);
if (_dcSession.Salts.Count > 48) return;
}
_dcSession.Salts[DateTime.MaxValue] = 0;
}
Task.Delay(5000).ContinueWith(_ => this.GetFutureSalts(128).ContinueWith(gfs =>
{
lock (_session)
{
_dcSession.Salts.Remove(DateTime.MaxValue);
foreach (var entry in gfs.Result.salts)
_dcSession.Salts[entry.valid_since] = entry.salt;
_dcSession.OldSalt = _dcSession.Salt;
_dcSession.Salt = _dcSession.Salts.Values[0];
_session.Save();
}
}));
}
internal MsgContainer ReadMsgContainer(BinaryReader reader)
{
int count = reader.ReadInt32();
var array = new _Message[count];
for (int i = 0; i < count; i++)
{
var msg = array[i] = new _Message(reader.ReadInt64(), reader.ReadInt32(), null) { bytes = reader.ReadInt32() };
if ((msg.seq_no & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msg.msg_id);
var pos = reader.BaseStream.Position;
try
{
var ctorNb = reader.ReadUInt32();
if (ctorNb == Layer.RpcResultCtor)
{
Helpers.Log(1, $" → {"RpcResult",-38} {MsgIdToStamp(msg.msg_id):u}");
msg.body = ReadRpcResult(reader);
}
else
{
var obj = msg.body = reader.ReadTLObject(ctorNb);
Helpers.Log(1, $" → {obj.GetType().Name,-38} {MsgIdToStamp(msg.msg_id):u} {((msg.seq_no & 1) != 0 ? "" : "(svc)")} {((msg.msg_id & 2) == 0 ? "" : "NAR")}");
}
}
catch (Exception ex)
{
Helpers.Log(4, "While deserializing vector<%Message>: " + ex.ToString());
}
reader.BaseStream.Position = pos + array[i].bytes;
}
return new MsgContainer { messages = array };
}
private RpcResult ReadRpcResult(BinaryReader reader)
{
long msgId = reader.ReadInt64();
var rpc = PullPendingRequest(msgId);
object result;
if (rpc != null)
{
try
{
if (!rpc.type.IsArray)
result = reader.ReadTLValue(rpc.type);
else
{
var peek = reader.ReadUInt32();
if (peek == Layer.RpcErrorCtor)
result = reader.ReadTLObject(Layer.RpcErrorCtor);
else if (peek == Layer.GZipedCtor)
using (var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress)))
result = gzipReader.ReadTLValue(rpc.type);
else
{
reader.BaseStream.Position -= 4;
result = reader.ReadTLValue(rpc.type);
}
}
if (rpc.type.IsEnum) result = Enum.ToObject(rpc.type, result);
if (result is RpcError rpcError)
Helpers.Log(4, $" → RpcError {rpcError.error_code,3} {rpcError.error_message,-24} #{(short)msgId.GetHashCode():X4}");
else
{
Helpers.Log(1, $" → {result?.GetType().Name,-37} #{(short)msgId.GetHashCode():X4}");
if (OnOwnUpdate != null)
if (result is UpdatesBase updates)
RaiseOwnUpdate(updates);
else if (result is Messages_AffectedMessages affected)
RaiseOwnUpdate(new UpdateShort { update = new UpdateAffectedMessages { affected = affected }, date = MsgIdToStamp(_lastRecvMsgId) });
}
rpc.tcs.SetResult(result);
}
catch (Exception ex)
{
rpc.tcs.SetException(ex);
throw;
}
}
else
{
var ctorNb = reader.ReadUInt32();
if (ctorNb == Layer.VectorCtor)
{
reader.BaseStream.Position -= 4;
result = reader.ReadTLVector(typeof(IObject[]));
}
else if (ctorNb == (uint)Bool.False) result = false;
else if (ctorNb == (uint)Bool.True) result = true;
else
{
result = reader.ReadTLObject(ctorNb);
if (OnOwnUpdate != null && result is UpdatesBase updates)
RaiseOwnUpdate(updates);
}
var typeName = result?.GetType().Name;
if (MsgIdToStamp(msgId) >= _session.SessionStart)
Helpers.Log(4, $" → {typeName,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}");
else
Helpers.Log(1, $" → {typeName,-37} for past msgId #{(short)msgId.GetHashCode():X4}");
}
return new RpcResult { req_msg_id = msgId, result = result };
}
class Rpc
{
internal Type type;
internal TaskCompletionSource