2021-08-04 00:40:09 +02:00
using System ;
2021-08-06 01:54:29 +02:00
using System.Buffers.Binary ;
2021-08-04 00:40:09 +02:00
using System.Collections.Generic ;
2021-12-05 07:21:30 +01:00
using System.ComponentModel ;
2021-08-04 00:40:09 +02:00
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Net ;
2024-09-30 02:15:10 +02:00
using System.Net.Http ;
2021-08-04 00:40:09 +02:00
using System.Net.Sockets ;
using System.Reflection ;
using System.Security.Cryptography ;
2021-08-13 00:28:34 +02:00
using System.Threading ;
2021-08-04 00:40:09 +02:00
using System.Threading.Tasks ;
2022-01-03 18:15:32 +01:00
using System.Web ;
2021-08-04 00:40:09 +02:00
using TL ;
using static WTelegram . Encryption ;
2022-10-12 23:25:15 +02:00
// necessary for .NET Standard 2.0 compilation:
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
2021-08-04 00:40:09 +02:00
namespace WTelegram
{
2024-09-07 01:59:27 +02:00
public partial class Client : IDisposable
#if NETCOREAPP2_1_OR_GREATER
, IAsyncDisposable
#endif
2021-08-04 00:40:09 +02:00
{
2022-06-14 00:58:51 +02:00
/// <summary>This event will be called when unsollicited updates/messages are sent by Telegram servers</summary>
2024-03-30 17:09:54 +01:00
/// <remarks>Make your handler <see langword="async"/>, or return <see cref="Task.CompletedTask"/> or <see langword="null"/><br/>See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30">Examples/Program_ReactorError.cs</see> for how to use this<br/>or <see href="https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21">Examples/Program_ListenUpdate.cs</see> using the UpdateManager class instead</remarks>
2024-03-29 16:42:58 +01:00
public event Func < UpdatesBase , Task > OnUpdates ;
2023-05-01 18:21:03 +02:00
/// <summary>This event is called for other types of notifications (login states, reactor errors, ...)</summary>
public event Func < IObject , Task > OnOther ;
2023-11-30 16:06:37 +01:00
/// <summary>Use this handler to intercept Updates that resulted from your own API calls</summary>
2024-03-29 16:42:58 +01:00
public event Func < UpdatesBase , Task > OnOwnUpdates ;
2021-11-06 05:22:33 +01:00
/// <summary>Used to create a TcpClient connected to the given address/port, or throw an exception on failure</summary>
2022-01-13 14:22:52 +01:00
public TcpFactory TcpHandler { get ; set ; } = DefaultTcpHandler ;
2022-02-11 02:43:48 +01:00
public delegate Task < TcpClient > TcpFactory ( string host , int port ) ;
2022-01-13 14:22:52 +01:00
/// <summary>Url for using a MTProxy. https://t.me/proxy?server=... </summary>
public string MTProxyUrl { get ; set ; }
2021-11-06 05:22:33 +01:00
/// <summary>Telegram configuration, obtained at connection time</summary>
2021-08-04 00:40:09 +02:00
public Config TLConfig { get ; private set ; }
2021-11-06 05:22:33 +01:00
/// <summary>Number of automatic reconnections on connection/reactor failure</summary>
public int MaxAutoReconnects { get ; set ; } = 5 ;
2022-05-31 14:28:37 +02:00
/// <summary>Number of attempts in case of wrong verification_code or password</summary>
public int MaxCodePwdAttempts { get ; set ; } = 3 ;
2021-11-12 20:50:39 +01:00
/// <summary>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</summary>
public int FloodRetryThreshold { get ; set ; } = 60 ;
2022-02-13 02:50:10 +01:00
/// <summary>Number of seconds between each keep-alive ping. Increase this if you have a slow connection or you're debugging your code</summary>
2021-12-07 15:26:53 +01:00
public int PingInterval { get ; set ; } = 60 ;
2022-02-24 16:44:27 +01:00
/// <summary>Size of chunks when uploading/downloading files. Reduce this if you don't have much memory</summary>
public int FilePartSize { get ; set ; } = 512 * 1024 ;
2021-11-06 05:22:33 +01:00
/// <summary>Is this Client instance the main or a secondary DC session</summary>
2024-08-10 23:29:08 +02:00
public bool IsMainDC = > _dcSession ? . DataCenter ? . flags . HasFlag ( DcOption . Flags . media_only ) ! = true
& & ( _dcSession ? . DataCenter ? . id - _session . MainDC ) is null or 0 ;
2021-12-01 15:50:35 +01:00
/// <summary>Has this Client established connection been disconnected?</summary>
2021-10-01 02:44:56 +02:00
public bool Disconnected = > _tcpClient ! = null & & ! ( _tcpClient . Client ? . Connected ? ? false ) ;
2022-03-23 13:50:43 +01:00
/// <summary>ID of the current logged-in user or 0</summary>
public long UserId = > _session . UserId ;
2024-03-19 11:48:45 +01:00
/// <summary>Info about the current logged-in user. This is only filled after a successful (re)login, not updated later</summary>
2022-09-20 17:30:32 +02:00
public User User { get ; private set ; }
2025-06-24 19:11:14 +02:00
/// <summary>Number of parallel transfers operations (uploads/downloads) allowed at the same time.</summary>
/// <remarks>Don't use this property while transfers are ongoing!</remarks>
public int ParallelTransfers
{
get = > _parallelTransfers . CurrentCount ;
set
{
int delta = value - _parallelTransfers . CurrentCount ;
for ( ; delta < 0 ; delta + + ) _parallelTransfers . Wait ( ) ;
if ( delta > 0 ) _parallelTransfers . Release ( delta ) ;
}
}
2022-01-03 18:15:32 +01:00
2022-09-20 17:30:32 +02:00
private Func < string , string > _config ;
2021-08-04 00:40:09 +02:00
private readonly Session _session ;
2022-01-25 16:37:09 +01:00
private string _apiHash ;
2021-09-28 16:12:20 +02:00
private Session . DCSession _dcSession ;
2021-08-04 00:40:09 +02:00
private TcpClient _tcpClient ;
2022-01-11 04:14:23 +01:00
private Stream _networkStream ;
2024-09-30 02:15:10 +02:00
private HttpClient _httpClient ;
2024-10-07 02:43:07 +02:00
private HttpWait _httpWait ;
2021-11-07 16:52:58 +01:00
private IObject _lastSentMsg ;
2021-08-14 15:15:41 +02:00
private long _lastRecvMsgId ;
2024-03-08 11:52:30 +01:00
private readonly List < long > _msgsToAck = [ ] ;
2021-08-10 14:40:41 +02:00
private readonly Random _random = new ( ) ;
2021-09-24 13:21:35 +02:00
private int _saltChangeCounter ;
2021-08-13 00:28:34 +02:00
private Task _reactorTask ;
2022-03-27 22:29:48 +02:00
private Rpc _bareRpc ;
2024-03-08 11:52:30 +01:00
private readonly Dictionary < long , Rpc > _pendingRpcs = [ ] ;
2021-09-16 04:47:15 +02:00
private SemaphoreSlim _sendSemaphore = new ( 0 ) ;
2021-09-28 16:12:20 +02:00
private readonly SemaphoreSlim _semaphore = new ( 1 ) ;
private Task _connecting ;
2021-08-13 00:28:34 +02:00
private CancellationTokenSource _cts ;
2021-09-18 07:26:06 +02:00
private int _reactorReconnects = 0 ;
2021-11-21 00:43:39 +01:00
private const string ConnectionShutDown = "Could not read payload length : Connection shut down" ;
2024-03-23 17:58:46 +01:00
private const long Ticks5Secs = 5 * TimeSpan . TicksPerSecond ;
2025-06-24 19:11:14 +02:00
private readonly SemaphoreSlim _parallelTransfers = new ( 2 ) ; // max parallel part uploads/downloads
2021-09-29 04:38:39 +02:00
private readonly SHA256 _sha256 = SHA256 . Create ( ) ;
private readonly SHA256 _sha256Recv = SHA256 . Create ( ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
private AesCtr _sendCtr , _recvCtr ;
#endif
private bool _paddedMode ;
2021-08-13 00:28:34 +02:00
2022-09-20 17:30:32 +02:00
public Client ( int apiID , string apiHash , string sessionPathname = null )
: this ( what = > what switch
{
"api_id" = > apiID . ToString ( ) ,
"api_hash" = > apiHash ,
"session_pathname" = > sessionPathname ,
_ = > null
} )
{ }
2024-02-18 17:41:30 +01:00
public Client ( Func < string , string > configProvider , byte [ ] startSession , Action < byte [ ] > saveSession )
: this ( configProvider , new ActionStore ( startSession , saveSession ) ) { }
2021-11-06 05:22:33 +01:00
/// <summary>Welcome to WTelegramClient! 🙂</summary>
/// <param name="configProvider">Config callback, is queried for: <b>api_id</b>, <b>api_hash</b>, <b>session_pathname</b></param>
2022-01-26 22:00:52 +01:00
/// <param name="sessionStore">if specified, must support initial Length & Read() of a session, then calls to Write() the updated session. Other calls can be ignored</param>
public Client ( Func < string , string > configProvider = null , Stream sessionStore = null )
2021-08-04 00:40:09 +02:00
{
_config = configProvider ? ? DefaultConfigOrAsk ;
2022-01-25 16:37:09 +01:00
var session_key = _config ( "session_key" ) ? ? ( _apiHash = Config ( "api_hash" ) ) ;
2023-02-26 17:09:45 +01:00
sessionStore ? ? = new SessionStore ( Config ( "session_pathname" ) ) ;
2022-01-26 22:00:52 +01:00
_session = Session . LoadOrCreate ( sessionStore , Convert . FromHexString ( session_key ) ) ;
2022-01-25 16:37:09 +01:00
if ( _session . ApiId = = 0 ) _session . ApiId = int . Parse ( Config ( "api_id" ) ) ;
2021-09-28 16:12:20 +02:00
if ( _session . MainDC ! = 0 ) _session . DCSessions . TryGetValue ( _session . MainDC , out _dcSession ) ;
2025-04-06 19:48:43 +02:00
_dcSession ? ? = new ( ) ;
2021-09-28 16:12:20 +02:00
_dcSession . Client = this ;
2021-11-04 03:10:03 +01:00
var version = Assembly . GetExecutingAssembly ( ) . GetCustomAttribute < AssemblyInformationalVersionAttribute > ( ) . InformationalVersion ;
2022-11-13 00:25:56 +01:00
Helpers . Log ( 1 , $"WTelegramClient {version} running under {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}" ) ;
2021-09-28 16:12:20 +02:00
}
private Client ( Client cloneOf , Session . DCSession dcSession )
{
_config = cloneOf . _config ;
_session = cloneOf . _session ;
2022-01-13 14:22:52 +01:00
TcpHandler = cloneOf . TcpHandler ;
MTProxyUrl = cloneOf . MTProxyUrl ;
PingInterval = cloneOf . PingInterval ;
2022-09-11 15:34:38 +02:00
TLConfig = cloneOf . TLConfig ;
2021-09-28 16:12:20 +02:00
_dcSession = dcSession ;
2021-08-04 00:40:09 +02:00
}
2021-12-05 11:47:52 +01:00
internal Task < string > ConfigAsync ( string what ) = > Task . Run ( ( ) = > Config ( what ) ) ;
internal string Config ( string what )
2023-04-02 13:44:23 +02:00
= > _config ( what ) ? ? DefaultConfig ( what ) ? ? throw new WTException ( "You must provide a config value for " + what ) ;
2021-08-04 12:03:43 +02:00
2021-11-06 05:22:33 +01:00
/// <summary>Default config values, used if your Config callback returns <see langword="null"/></summary>
2021-12-05 11:47:52 +01:00
public static string DefaultConfig ( string what ) = > what switch
2021-08-04 00:40:09 +02:00
{
"session_pathname" = > Path . Combine (
2022-01-07 00:24:47 +01:00
Path . GetDirectoryName ( Path . GetDirectoryName ( AppDomain . CurrentDomain . BaseDirectory . TrimEnd ( Path . DirectorySeparatorChar ) ) )
? ? AppDomain . CurrentDomain . BaseDirectory , "WTelegram.session" ) ,
2021-08-04 00:40:09 +02:00
#if DEBUG
2024-02-19 17:08:26 +01:00
"server_address" = > "2>149.154.167.40:443" , // Test DC 2
2021-08-04 00:40:09 +02:00
#else
2024-02-19 17:08:26 +01:00
"server_address" = > "2>149.154.167.50:443" , // DC 2
2021-08-04 00:40:09 +02:00
#endif
"device_model" = > Environment . Is64BitOperatingSystem ? "PC 64bit" : "PC 32bit" ,
2022-01-25 16:37:09 +01:00
"system_version" = > Helpers . GetSystemVersion ( ) ,
"app_version" = > Helpers . GetAppVersion ( ) ,
2021-08-04 00:40:09 +02:00
"system_lang_code" = > CultureInfo . InstalledUICulture . TwoLetterISOLanguageName ,
"lang_pack" = > "" ,
"lang_code" = > CultureInfo . CurrentUICulture . TwoLetterISOLanguageName ,
2021-09-02 00:39:06 +02:00
"user_id" = > "-1" ,
2022-09-14 18:29:07 +02:00
"verification_code" or "email_verification_code" or "password" = > AskConfig ( what ) ,
2023-11-11 12:25:02 +01:00
"init_params" = > "{}" ,
2021-10-11 14:44:49 +02:00
_ = > null // api_id api_hash phone_number... it's up to you to reply to these correctly
2021-08-04 00:40:09 +02:00
} ;
2021-11-06 05:22:33 +01:00
internal static string DefaultConfigOrAsk ( string config ) = > DefaultConfig ( config ) ? ? AskConfig ( config ) ;
2021-10-11 14:44:49 +02:00
private static string AskConfig ( string config )
2021-08-04 00:40:09 +02:00
{
2022-01-29 16:47:47 +01:00
if ( config = = "session_key" )
{
Console . WriteLine ( "Welcome! You can obtain your api_id/api_hash at https://my.telegram.org/apps" ) ;
return null ;
}
2021-08-04 00:40:09 +02:00
Console . Write ( $"Enter {config.Replace('_', ' ')}: " ) ;
return Console . ReadLine ( ) ;
}
2021-11-06 05:22:33 +01:00
/// <summary>Load a specific Telegram server public key</summary>
/// <param name="pem">A string starting with <c>-----BEGIN RSA PUBLIC KEY-----</c></param>
public static void LoadPublicKey ( string pem ) = > Encryption . LoadPublicKey ( pem ) ;
2021-08-04 00:40:09 +02:00
2021-12-14 13:39:43 +01:00
/// <summary>Builds a structure that is used to validate a 2FA password</summary>
2023-05-17 18:26:53 +02:00
/// <param name="accountPassword">Password validation configuration. You can obtain this via <c>Account_GetPassword</c> or through OnOther as part of the login process</param>
2021-12-14 13:39:43 +01:00
/// <param name="password">The password to validate</param>
public static Task < InputCheckPasswordSRP > InputCheckPassword ( Account_Password accountPassword , string password )
= > Check2FA ( accountPassword , ( ) = > Task . FromResult ( password ) ) ;
2024-09-06 18:22:05 +02:00
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816")]
public void Dispose ( ) = > DisposeAsync ( ) . AsTask ( ) . Wait ( ) ;
public async ValueTask DisposeAsync ( )
2021-08-13 00:28:34 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 2 , $"{_dcSession.DcID}>Disposing the client" ) ;
2024-09-06 18:22:05 +02:00
await ResetAsync ( false , IsMainDC ) . ConfigureAwait ( false ) ;
2024-06-15 02:35:38 +02:00
var ex = new ObjectDisposedException ( "WTelegram.Client was disposed" ) ;
2023-11-29 15:16:35 +01:00
lock ( _pendingRpcs ) // abort all pending requests
foreach ( var rpc in _pendingRpcs . Values )
rpc . tcs . TrySetException ( ex ) ;
2024-04-24 17:25:45 +02:00
_sendSemaphore . Dispose ( ) ;
2024-10-07 02:43:07 +02:00
_httpClient ? . Dispose ( ) ;
2022-01-17 16:54:34 +01:00
_networkStream = null ;
2022-01-23 01:28:10 +01:00
if ( IsMainDC ) _session . Dispose ( ) ;
2021-12-10 14:19:28 +01:00
GC . SuppressFinalize ( this ) ;
2021-08-13 00:28:34 +02:00
}
2022-07-12 01:31:18 +02:00
public void DisableUpdates ( bool disable = true ) = > _dcSession . DisableUpdates ( disable ) ;
2024-10-07 02:43:07 +02:00
2024-09-30 02:15:10 +02:00
/// <summary>Enable connecting to Telegram via on-demand HTTP requests instead of permanent TCP connection</summary>
/// <param name="httpClient">HttpClient to use. Leave <see langword="null"/> for a default one</param>
2024-10-07 02:43:07 +02:00
/// <param name="defaultHttpWait">Default HttpWait parameters for requests.<para>⚠️ Telegram servers don't support this correctly at the moment.</para>So leave <see langword="null"/> for the default 25 seconds long poll</param>
public void HttpMode ( HttpClient httpClient = null , HttpWait defaultHttpWait = null )
{
if ( _tcpClient ! = null ) throw new InvalidOperationException ( "Cannot switch to HTTP after TCP connection" ) ;
_httpClient = httpClient ? ? new ( ) ;
_httpWait = defaultHttpWait ;
2025-06-24 19:11:14 +02:00
ParallelTransfers = 1 ;
2024-10-07 02:43:07 +02:00
}
2022-07-12 01:31:18 +02:00
2021-11-06 05:22:33 +01:00
/// <summary>Disconnect from Telegram <i>(shouldn't be needed in normal usage)</i></summary>
/// <param name="resetUser">Forget about logged-in user</param>
/// <param name="resetSessions">Disconnect secondary sessions with other DCs</param>
2024-09-06 18:22:05 +02:00
public void Reset ( bool resetUser = true , bool resetSessions = true ) = > ResetAsync ( resetUser , resetSessions ) . Wait ( ) ;
/// <summary>Disconnect from Telegram <i>(shouldn't be needed in normal usage)</i></summary>
/// <param name="resetUser">Forget about logged-in user</param>
/// <param name="resetSessions">Disconnect secondary sessions with other DCs</param>
public async Task ResetAsync ( bool resetUser = true , bool resetSessions = true )
2021-08-06 07:28:54 +02:00
{
2021-10-01 02:22:26 +02:00
try
{
2024-11-18 13:44:32 +01:00
if ( _httpClient = = null & & CheckMsgsToAck ( ) is MsgsAck msgsAck )
2024-09-06 18:22:05 +02:00
await SendAsync ( msgsAck , false ) . WaitAsync ( 1000 ) . ConfigureAwait ( false ) ;
2021-10-01 02:22:26 +02:00
}
2022-05-09 23:43:06 +02:00
catch { }
2021-08-13 00:28:34 +02:00
_cts ? . Cancel ( ) ;
2022-09-19 22:28:12 +02:00
_sendSemaphore = new ( 0 ) ; // initially taken, first released during DoConnectAsync
2022-05-09 23:43:06 +02:00
try
{
2024-09-06 18:22:05 +02:00
await _reactorTask . WaitAsync ( 1000 ) . ConfigureAwait ( false ) ;
2022-05-09 23:43:06 +02:00
}
catch { }
2023-05-18 21:50:19 +02:00
_reactorTask = resetSessions ? null : Task . CompletedTask ;
2022-01-11 04:42:41 +01:00
_networkStream ? . Close ( ) ;
2021-08-13 00:28:34 +02:00
_tcpClient ? . Dispose ( ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
_sendCtr ? . Dispose ( ) ;
_recvCtr ? . Dispose ( ) ;
#endif
_paddedMode = false ;
2021-09-28 16:12:20 +02:00
_connecting = null ;
2022-08-29 02:37:30 +02:00
_bareRpc = null ;
2021-09-23 09:27:52 +02:00
if ( resetSessions )
{
2021-09-28 16:12:20 +02:00
foreach ( var altSession in _session . DCSessions . Values )
if ( altSession . Client ! = null & & altSession . Client ! = this )
{
2024-09-07 18:43:52 +02:00
await altSession . Client . DisposeAsync ( ) ;
2021-09-28 16:12:20 +02:00
altSession . Client = null ;
}
2021-09-23 09:27:52 +02:00
}
2021-09-29 02:51:48 +02:00
if ( resetUser )
2022-09-20 17:30:32 +02:00
{
_loginCfg = default ;
2021-11-07 09:09:15 +01:00
_session . UserId = 0 ;
2022-09-20 17:30:32 +02:00
User = null ;
}
2021-08-06 07:28:54 +02:00
}
2022-04-06 18:38:54 +02:00
private Session . DCSession GetOrCreateDCSession ( int dcId , DcOption . Flags flags )
2021-08-04 00:40:09 +02:00
{
2024-07-20 02:13:56 +02:00
if ( _session . DCSessions . TryGetValue ( dcId , out var dcSession ) & & dcSession . AuthKey ! = null )
2022-04-06 18:38:54 +02:00
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
2024-07-20 02:13:56 +02:00
if ( dcSession = = null & & _session . DCSessions . TryGetValue ( - dcId , out dcSession ) & & dcSession . AuthKey ! = null )
{
2024-09-05 18:51:53 +02:00
// we have already negociated an AuthKey with this DC
2024-07-20 02:13:56 +02:00
if ( dcSession . DataCenter . flags = = flags & & _session . DCSessions . Remove ( - dcId ) )
return _session . DCSessions [ dcId ] = dcSession ; // we found a misclassed DC, change its sign
2025-04-06 19:48:43 +02:00
dcSession = new Session . DCSession { // clone AuthKey for a session on the matching media_only DC
authKeyID = dcSession . authKeyID , AuthKey = dcSession . AuthKey , UserId = dcSession . UserId } ;
2024-07-20 02:13:56 +02:00
}
2022-04-06 18:38:54 +02:00
// try to find the most appropriate DcOption for this DC
2024-07-20 02:13:56 +02:00
if ( dcSession ? . AuthKey = = null ) // we'll need to negociate an AuthKey => can't use media_only DC
{
2022-04-06 18:38:54 +02:00
flags & = ~ DcOption . Flags . media_only ;
2024-07-20 02:13:56 +02:00
dcId = Math . Abs ( dcId ) ;
}
2024-09-07 18:50:07 +02:00
var dcOptions = GetDcOptions ( Math . Abs ( dcId ) , flags ) ;
2023-04-02 13:44:23 +02:00
var dcOption = dcOptions . FirstOrDefault ( ) ? ? throw new WTException ( $"Could not find adequate dc_option for DC {dcId}" ) ;
2025-04-06 19:48:43 +02:00
dcSession ? ? = new ( ) ; // create new session only if not already existing
2022-04-06 18:38:54 +02:00
dcSession . DataCenter = dcOption ;
return _session . DCSessions [ dcId ] = dcSession ;
2021-08-04 00:40:09 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Obtain/create a Client for a secondary session on a specific Data Center</summary>
2024-07-20 02:13:56 +02:00
/// <param name="dcId">ID of the Data Center (use negative values for media_only)</param>
2021-11-06 05:22:33 +01:00
/// <param name="connect">Connect immediately</param>
2022-04-06 18:38:54 +02:00
/// <returns>Client connected to the selected DC</returns>
2025-03-03 02:02:06 +01:00
/// <remarks>⚠️ You shouldn't have to use this method unless you know what you're doing</remarks>
2024-07-20 02:13:56 +02:00
public async Task < Client > GetClientForDC ( int dcId , bool connect = true )
2021-08-04 00:40:09 +02:00
{
2021-09-28 16:12:20 +02:00
if ( _dcSession . DataCenter ? . id = = dcId ) return this ;
Session . DCSession altSession ;
lock ( _session )
{
2024-07-20 02:13:56 +02:00
var flags = _dcSession . DataCenter . flags ;
if ( dcId < 0 ) flags = ( flags & DcOption . Flags . ipv6 ) | DcOption . Flags . media_only ;
altSession = GetOrCreateDCSession ( dcId , flags ) ;
2025-04-06 19:48:43 +02:00
_session . Save ( ) ;
2021-10-01 02:44:56 +02:00
if ( altSession . Client ? . Disconnected ? ? false ) { altSession . Client . Dispose ( ) ; altSession . Client = null ; }
2021-09-28 16:12:20 +02:00
altSession . Client ? ? = new Client ( this , altSession ) ;
}
Helpers . Log ( 2 , $"Requested connection to DC {dcId}..." ) ;
if ( connect )
{
await _semaphore . WaitAsync ( ) ;
try
{
Auth_ExportedAuthorization exported = null ;
2025-06-26 22:02:26 +02:00
if ( _session . UserId ! = 0 & & IsMainDC & & altSession . UserId ! = _session . UserId & & Math . Abs ( altSession . DcID ) ! = Math . Abs ( _dcSession . DcID ) )
2024-07-20 02:13:56 +02:00
exported = await this . Auth_ExportAuthorization ( Math . Abs ( dcId ) ) ;
2021-09-28 16:12:20 +02:00
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 } )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Failed to get Authorization: " + authorization . GetType ( ) . Name ) ;
2021-09-28 16:12:20 +02:00
altSession . UserId = user . id ;
2025-04-06 19:48:43 +02:00
lock ( _session ) _session . Save ( ) ;
2021-09-28 16:12:20 +02:00
}
}
finally
{
_semaphore . Release ( ) ;
}
}
return altSession . Client ;
}
2025-06-26 22:02:26 +02:00
private async Task Reactor ( Stream stream , CancellationToken ct )
2021-09-17 03:12:23 +02:00
{
2021-09-23 05:37:00 +02:00
const int MinBufferSize = 1024 ;
var data = new byte [ MinBufferSize ] ;
2025-06-26 22:02:26 +02:00
while ( ! ct . IsCancellationRequested )
2021-09-17 03:12:23 +02:00
{
2021-11-07 16:52:58 +01:00
IObject obj = null ;
2021-09-16 04:47:15 +02:00
try
2021-08-13 00:28:34 +02:00
{
2025-06-26 22:02:26 +02:00
if ( await stream . FullReadAsync ( data , 4 , ct ) ! = 4 )
2023-04-02 13:44:23 +02:00
throw new WTException ( ConnectionShutDown ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
_recvCtr . EncryptDecrypt ( data , 4 ) ;
#endif
2021-09-23 05:37:00 +02:00
int payloadLen = BinaryPrimitives . ReadInt32LittleEndian ( data ) ;
2022-01-11 04:14:23 +01:00
if ( payloadLen < = 0 )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Could not read frame data : Invalid payload length" ) ;
2022-01-11 04:14:23 +01:00
else if ( payloadLen > data . Length )
2021-09-23 05:37:00 +02:00
data = new byte [ payloadLen ] ;
else if ( Math . Max ( payloadLen , MinBufferSize ) < data . Length / 4 )
data = new byte [ Math . Max ( payloadLen , MinBufferSize ) ] ;
2025-06-26 22:02:26 +02:00
if ( await stream . FullReadAsync ( data , payloadLen , ct ) ! = payloadLen )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Could not read frame data : Connection shut down" ) ;
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
_recvCtr . EncryptDecrypt ( data , payloadLen ) ;
#endif
2021-09-23 05:37:00 +02:00
obj = ReadFrame ( data , payloadLen ) ;
2021-08-13 00:28:34 +02:00
}
2021-09-16 04:47:15 +02:00
catch ( Exception ex ) // an exception in RecvAsync is always fatal
2021-08-13 00:28:34 +02:00
{
2025-06-26 22:02:26 +02:00
if ( ct . IsCancellationRequested ) return ;
2023-04-02 13:44:23 +02:00
bool disconnectedAltDC = ! IsMainDC & & ex is WTException { Message : ConnectionShutDown } or IOException { InnerException : SocketException } ;
2022-12-02 01:42:39 +01:00
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}" ) ;
2021-09-16 04:47:15 +02:00
var oldSemaphore = _sendSemaphore ;
2025-06-26 22:02:26 +02:00
await oldSemaphore . WaitAsync ( ct ) ; // prevent any sending while we reconnect
2021-09-28 16:12:20 +02:00
var reactorError = new ReactorError { Exception = ex } ;
2021-09-16 04:47:15 +02:00
try
2021-09-28 16:12:20 +02:00
{
2021-11-21 00:43:39 +01:00
lock ( _msgsToAck ) _msgsToAck . Clear ( ) ;
2024-09-06 18:22:05 +02:00
await ResetAsync ( false , false ) ;
2021-09-28 16:12:20 +02:00
_reactorReconnects = ( _reactorReconnects + 1 ) % MaxAutoReconnects ;
2022-12-02 01:42:39 +01:00
if ( disconnectedAltDC & & _pendingRpcs . Count < = 1 )
2022-03-27 22:29:48 +02:00
if ( _pendingRpcs . Values . FirstOrDefault ( ) is not Rpc rpc | | rpc . type = = typeof ( Pong ) )
2021-11-21 00:43:39 +01:00
_reactorReconnects = 0 ;
2022-11-01 18:44:01 +01:00
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
2021-09-28 16:12:20 +02:00
{
2022-11-01 18:44:01 +01:00
foreach ( var rpc in _pendingRpcs . Values )
2024-06-15 02:35:38 +02:00
rpc . tcs . TrySetResult ( reactorError ) ; // this leads to a retry (see Invoke<T> method)
2022-11-01 18:44:01 +01:00
_pendingRpcs . Clear ( ) ;
_bareRpc = null ;
}
if ( IsMainDC )
{
var updatesState = await this . Updates_GetState ( ) ; // this call reenables incoming Updates
2024-03-29 16:42:58 +01:00
RaiseUpdates ( updatesState ) ;
2021-09-28 16:12:20 +02:00
}
}
2024-06-15 02:35:38 +02:00
catch ( Exception e ) when ( e is not ObjectDisposedException )
2021-09-16 04:47:15 +02:00
{
2023-03-09 22:32:57 +01:00
if ( IsMainDC )
2024-03-29 16:42:58 +01:00
RaiseUpdates ( reactorError ) ;
2022-03-27 22:29:48 +02:00
lock ( _pendingRpcs ) // abort all pending requests
2021-09-16 04:47:15 +02:00
{
2022-03-27 22:29:48 +02:00
foreach ( var rpc in _pendingRpcs . Values )
2024-05-28 00:37:58 +02:00
rpc . tcs . TrySetException ( ex ) ;
2022-03-27 22:29:48 +02:00
_pendingRpcs . Clear ( ) ;
_bareRpc = null ;
2021-09-16 04:47:15 +02:00
}
}
finally
{
oldSemaphore . Release ( ) ;
}
2021-08-13 00:28:34 +02:00
}
2021-09-16 04:47:15 +02:00
if ( obj ! = null )
await HandleMessageAsync ( obj ) ;
2021-08-13 07:06:44 +02:00
}
}
2022-04-06 18:38:54 +02:00
internal DateTime MsgIdToStamp ( long serverMsgId )
2025-04-06 19:48:43 +02:00
= > new ( ( serverMsgId > > 32 ) * 10000000 - _dcSession . serverTicksOffset + 621355968000000000L , DateTimeKind . Utc ) ;
2022-04-06 18:38:54 +02:00
2021-11-07 16:52:58 +01:00
internal IObject ReadFrame ( byte [ ] data , int dataLen )
2021-08-04 00:40:09 +02:00
{
2022-06-19 18:08:19 +02:00
if ( dataLen < 8 & & data [ 3 ] = = 0xFF )
2021-08-06 07:28:54 +02:00
{
int error_code = - BinaryPrimitives . ReadInt32LittleEndian ( data ) ;
throw new RpcException ( error_code , TransportError ( error_code ) ) ;
}
2021-09-23 05:37:00 +02:00
if ( dataLen < 24 ) // authKeyId+msgId+length+ctorNb | authKeyId+msgKey
2023-04-02 13:44:23 +02:00
throw new WTException ( $"Packet payload too small: {dataLen}" ) ;
2021-08-04 00:40:09 +02:00
2021-08-06 01:54:29 +02:00
long authKeyId = BinaryPrimitives . ReadInt64LittleEndian ( data ) ;
2025-04-06 19:48:43 +02:00
if ( authKeyId ! = _dcSession . authKeyID )
2023-04-02 13:44:23 +02:00
throw new WTException ( $"Received a packet encrypted with unexpected key {authKeyId:X}" ) ;
2021-08-04 00:40:09 +02:00
if ( authKeyId = = 0 ) // Unencrypted message
{
2023-02-14 11:14:17 +01:00
using var reader = new BinaryReader ( new MemoryStream ( data , 8 , dataLen - 8 ) ) ;
2021-08-14 15:15:41 +02:00
long msgId = _lastRecvMsgId = reader . ReadInt64 ( ) ;
2023-04-02 13:44:23 +02:00
if ( ( msgId & 1 ) = = 0 ) throw new WTException ( $"Invalid server msgId {msgId}" ) ;
2021-08-04 00:40:09 +02:00
int length = reader . ReadInt32 ( ) ;
2022-01-03 18:15:32 +01:00
dataLen - = 20 ;
2023-07-05 05:03:08 +02:00
if ( length > dataLen | | dataLen - length > ( _paddedMode ? 256 : 0 ) )
2023-04-02 13:44:23 +02:00
throw new WTException ( $"Unexpected unencrypted/padding length {dataLen} - {length}" ) ;
2021-08-04 00:40:09 +02:00
2021-08-12 12:37:56 +02:00
var obj = reader . ReadTLObject ( ) ;
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {obj.GetType().Name,-40} {MsgIdToStamp(msgId):u} clear{((msgId & 2) == 0 ? "" : " NAR ")}" ) ;
2023-04-02 13:44:23 +02:00
if ( _bareRpc = = null ) throw new WTException ( "Shouldn't receive unencrypted packet at this point" ) ;
2021-08-12 12:37:56 +02:00
return obj ;
2021-08-04 00:40:09 +02:00
}
else
{
2022-10-01 13:56:43 +02:00
byte [ ] decrypted_data = EncryptDecryptMessage ( data . AsSpan ( 24 , ( dataLen - 24 ) & ~ 0xF ) , false , 8 , _dcSession . AuthKey , data , 8 , _sha256Recv ) ;
2021-08-04 00:40:09 +02:00
if ( decrypted_data . Length < 36 ) // header below+ctorNb
2023-04-02 13:44:23 +02:00
throw new WTException ( $"Decrypted packet too small: {decrypted_data.Length}" ) ;
2022-05-19 01:32:22 +02:00
_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 ) ) )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Mismatch between MsgKey & decrypted SHA256" ) ;
2022-05-19 01:32:22 +02:00
_sha256Recv . Initialize ( ) ;
2023-02-14 11:14:17 +01:00
using var reader = new BinaryReader ( new MemoryStream ( decrypted_data ) ) ;
2021-12-30 23:45:28 +01:00
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
2022-09-19 22:28:12 +02:00
2023-04-02 13:44:23 +02:00
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}" ) ;
2025-04-06 19:48:43 +02:00
if ( sessionId ! = _dcSession . id ) throw new WTException ( $"Unexpected session ID: {sessionId} != {_dcSession.id}" ) ;
2023-04-02 13:44:23 +02:00
if ( ( msgId & 1 ) = = 0 ) throw new WTException ( $"msg_id is not odd: {msgId}" ) ;
2022-05-19 01:32:22 +02:00
if ( ! _dcSession . CheckNewMsgId ( msgId ) )
{
Helpers . Log ( 3 , $"{_dcSession.DcID}>Ignoring duplicate or old msg_id {msgId}" ) ;
return null ;
}
2024-02-21 14:51:26 +01:00
var utcNow = DateTime . UtcNow ;
2021-12-30 23:45:28 +01:00
if ( _lastRecvMsgId = = 0 ) // resync ServerTicksOffset on first message
2025-04-06 19:48:43 +02:00
_dcSession . serverTicksOffset = ( msgId > > 32 ) * 10000000 - utcNow . Ticks + 621355968000000000L ;
2021-12-30 23:45:28 +01:00
var msgStamp = MsgIdToStamp ( _lastRecvMsgId = msgId ) ;
2024-03-23 17:58:46 +01:00
long deltaTicks = ( msgStamp - utcNow ) . Ticks ;
if ( deltaTicks is > 0 )
if ( deltaTicks < Ticks5Secs ) // resync if next message is less than 5 seconds in the future
2025-04-06 19:48:43 +02:00
_dcSession . serverTicksOffset + = deltaTicks ;
else if ( _dcSession . serverTicksOffset < - Ticks5Secs & & deltaTicks + _dcSession . serverTicksOffset < 0 )
_dcSession . serverTicksOffset + = deltaTicks ;
2023-12-18 00:01:07 +01:00
if ( serverSalt ! = _dcSession . Salt & & serverSalt ! = _dcSession . OldSalt & & serverSalt ! = _dcSession . Salts ? . Values . ElementAtOrDefault ( 1 ) )
2021-08-05 16:29:58 +02:00
{
2023-05-02 22:49:38 +02:00
Helpers . Log ( 3 , $"{_dcSession.DcID}>Server salt has changed: {_dcSession.Salt:X} -> {serverSalt:X}" ) ;
2023-12-18 00:01:07 +01:00
_dcSession . OldSalt = _dcSession . Salt ;
2021-09-28 16:12:20 +02:00
_dcSession . Salt = serverSalt ;
2025-04-06 19:48:43 +02:00
lock ( _session ) _session . Save ( ) ;
2023-05-02 22:49:38 +02:00
if ( + + _saltChangeCounter > = 10 )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Server salt changed too often! Security issue?" ) ;
2023-05-02 22:49:38 +02:00
CheckSalt ( ) ;
2021-08-05 16:29:58 +02:00
}
2021-09-16 04:47:15 +02:00
if ( ( seqno & 1 ) ! = 0 ) lock ( _msgsToAck ) _msgsToAck . Add ( msgId ) ;
2022-02-11 02:43:48 +01:00
2021-08-13 07:06:44 +02:00
var ctorNb = reader . ReadUInt32 ( ) ;
2024-03-23 17:58:46 +01:00
if ( ctorNb ! = Layer . BadMsgCtor & & deltaTicks / TimeSpan . TicksPerSecond is > 30 or < - 300 )
2022-04-06 18:38:54 +02:00
{ // msg_id values that belong over 30 seconds in the future or over 300 seconds in the past are to be ignored.
2025-04-06 19:48:43 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Ignoring 0x{ctorNb:X8} because of wrong timestamp {msgStamp:u} - {utcNow:u} Δ={new TimeSpan(_dcSession.serverTicksOffset):c}" ) ;
2021-12-30 23:45:28 +01:00
return null ;
}
2024-06-15 17:35:25 +02:00
try
2021-08-13 07:06:44 +02:00
{
2024-06-15 17:35:25 +02:00
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 ;
}
2021-08-13 07:06:44 +02:00
}
2024-06-15 17:35:25 +02:00
catch ( Exception ex )
2021-08-13 07:06:44 +02:00
{
2024-06-15 17:35:25 +02:00
Helpers . Log ( 4 , $"While deserializing frame #{ctorNb:x}: " + ex . ToString ( ) ) ;
return null ;
2021-08-13 07:06:44 +02:00
}
2021-08-12 12:37:56 +02:00
}
2021-08-04 00:40:09 +02:00
}
2024-09-30 02:15:10 +02:00
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"
} ;
2023-05-02 22:49:38 +02:00
internal void CheckSalt ( )
{
lock ( _session )
{
2024-03-08 11:52:30 +01:00
_dcSession . Salts ? ? = [ ] ;
2023-05-02 22:49:38 +02:00
if ( _dcSession . Salts . Count ! = 0 )
{
var keys = _dcSession . Salts . Keys ;
if ( keys [ ^ 1 ] = = DateTime . MaxValue ) return ; // GetFutureSalts ongoing
2025-04-06 19:48:43 +02:00
var now = DateTime . UtcNow . AddTicks ( _dcSession . serverTicksOffset ) ;
bool removed = false ;
for ( ; keys . Count > 1 & & keys [ 1 ] < now ; _dcSession . OldSalt = _dcSession . Salt , _dcSession . Salt = _dcSession . Salts . Values [ 0 ] , removed = true )
2023-05-02 22:49:38 +02:00
_dcSession . Salts . RemoveAt ( 0 ) ;
2025-04-06 19:48:43 +02:00
if ( removed ) _session . Save ( ) ;
2023-05-02 22:49:38 +02:00
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 ;
2023-12-18 00:01:07 +01:00
_dcSession . OldSalt = _dcSession . Salt ;
2023-05-02 22:49:38 +02:00
_dcSession . Salt = _dcSession . Salts . Values [ 0 ] ;
_session . Save ( ) ;
}
} ) ) ;
}
2023-02-14 11:14:17 +01:00
internal MsgContainer ReadMsgContainer ( BinaryReader reader )
2021-09-16 04:47:15 +02:00
{
2022-04-06 18:38:54 +02:00
int count = reader . ReadInt32 ( ) ;
2024-10-07 02:43:07 +02:00
var messages = new List < _Message > ( count ) ;
2022-04-06 18:38:54 +02:00
for ( int i = 0 ; i < count ; i + + )
2021-08-04 00:40:09 +02:00
{
2024-10-07 02:43:07 +02:00
var msg = new _Message ( reader . ReadInt64 ( ) , reader . ReadInt32 ( ) , null ) { bytes = reader . ReadInt32 ( ) } ;
messages . Add ( msg ) ;
2024-02-25 03:09:49 +01:00
if ( ( msg . seq_no & 1 ) ! = 0 ) lock ( _msgsToAck ) _msgsToAck . Add ( msg . msg_id ) ;
2021-08-13 07:06:44 +02:00
var pos = reader . BaseStream . Position ;
try
{
var ctorNb = reader . ReadUInt32 ( ) ;
2021-09-17 04:53:02 +02:00
if ( ctorNb = = Layer . RpcResultCtor )
2021-08-13 07:06:44 +02:00
{
2025-06-26 22:02:26 +02:00
Helpers . Log ( 1 , $" → {" RpcResult ",-38} {MsgIdToStamp(msg.msg_id):u}" ) ;
2021-08-13 07:06:44 +02:00
msg . body = ReadRpcResult ( reader ) ;
}
else
{
var obj = msg . body = reader . ReadTLObject ( ctorNb ) ;
2025-06-26 22:02:26 +02:00
Helpers . Log ( 1 , $" → {obj.GetType().Name,-38} {MsgIdToStamp(msg.msg_id):u} {((msg.seq_no & 1) != 0 ? "" : " ( svc ) ")} {((msg.msg_id & 2) == 0 ? " " : " NAR ")}" ) ;
2021-08-13 07:06:44 +02:00
}
}
catch ( Exception ex )
{
Helpers . Log ( 4 , "While deserializing vector<%Message>: " + ex . ToString ( ) ) ;
}
2024-10-07 02:43:07 +02:00
reader . BaseStream . Position = pos + msg . bytes ;
2021-08-05 16:29:58 +02:00
}
2024-10-07 02:43:07 +02:00
return new MsgContainer { messages = messages } ;
2021-08-04 00:40:09 +02:00
}
2023-02-14 11:14:17 +01:00
private RpcResult ReadRpcResult ( BinaryReader reader )
2021-08-04 00:40:09 +02:00
{
2021-08-13 07:06:44 +02:00
long msgId = reader . ReadInt64 ( ) ;
2022-03-27 22:29:48 +02:00
var rpc = PullPendingRequest ( msgId ) ;
2021-08-14 08:55:30 +02:00
object result ;
2022-03-27 22:29:48 +02:00
if ( rpc ! = null )
2021-08-13 00:28:34 +02:00
{
2021-12-07 16:35:23 +01:00
try
{
2022-03-27 22:29:48 +02:00
if ( ! rpc . type . IsArray )
result = reader . ReadTLValue ( rpc . type ) ;
2021-12-07 16:35:23 +01:00
else
{
2022-01-17 15:06:29 +01:00
var peek = reader . ReadUInt32 ( ) ;
if ( peek = = Layer . RpcErrorCtor )
result = reader . ReadTLObject ( Layer . RpcErrorCtor ) ;
else if ( peek = = Layer . GZipedCtor )
2024-04-14 13:25:45 +02:00
result = reader . ReadTLGzipped ( rpc . type ) ;
2022-01-17 15:06:29 +01:00
else
{
reader . BaseStream . Position - = 4 ;
2024-03-28 12:13:56 +01:00
result = reader . ReadTLVector ( rpc . type ) ;
2022-01-17 15:06:29 +01:00
}
2021-12-07 16:35:23 +01:00
}
2022-03-27 22:29:48 +02:00
if ( rpc . type . IsEnum ) result = Enum . ToObject ( rpc . type , result ) ;
2021-12-21 02:28:35 +01:00
if ( result is RpcError rpcError )
Helpers . Log ( 4 , $" → RpcError {rpcError.error_code,3} {rpcError.error_message,-24} #{(short)msgId.GetHashCode():X4}" ) ;
else
2023-11-30 16:06:37 +01:00
{
2021-12-21 02:28:35 +01:00
Helpers . Log ( 1 , $" → {result?.GetType().Name,-37} #{(short)msgId.GetHashCode():X4}" ) ;
2024-11-22 16:27:21 +01:00
CheckRaiseOwnUpdates ( result ) ;
2023-11-30 16:06:37 +01:00
}
2022-03-27 22:29:48 +02:00
rpc . tcs . SetResult ( result ) ;
2021-12-07 16:35:23 +01:00
}
catch ( Exception ex )
2021-10-01 02:22:26 +02:00
{
2022-03-27 22:29:48 +02:00
rpc . tcs . SetException ( ex ) ;
2021-12-07 16:35:23 +01:00
throw ;
2021-10-01 02:22:26 +02:00
}
2021-08-13 00:28:34 +02:00
}
2021-08-04 00:40:09 +02:00
else
2021-08-13 00:28:34 +02:00
{
2022-01-11 04:14:23 +01:00
var ctorNb = reader . ReadUInt32 ( ) ;
if ( ctorNb = = Layer . VectorCtor )
{
reader . BaseStream . Position - = 4 ;
2022-02-03 16:55:16 +01:00
result = reader . ReadTLVector ( typeof ( IObject [ ] ) ) ;
2022-01-11 04:14:23 +01:00
}
2022-02-03 16:55:16 +01:00
else if ( ctorNb = = ( uint ) Bool . False ) result = false ;
else if ( ctorNb = = ( uint ) Bool . True ) result = true ;
2023-11-30 16:06:37 +01:00
else
{
result = reader . ReadTLObject ( ctorNb ) ;
2024-11-22 16:27:21 +01:00
CheckRaiseOwnUpdates ( result ) ;
2023-11-30 16:06:37 +01:00
}
2022-02-03 16:55:16 +01:00
var typeName = result ? . GetType ( ) . Name ;
2021-09-28 16:12:20 +02:00
if ( MsgIdToStamp ( msgId ) > = _session . SessionStart )
2025-06-26 22:02:26 +02:00
Helpers . Log ( 4 , $" → {typeName,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}" ) ;
2021-08-13 00:28:34 +02:00
else
2025-06-26 22:02:26 +02:00
Helpers . Log ( 1 , $" → {typeName,-37} for past msgId #{(short)msgId.GetHashCode():X4}" ) ;
2021-08-13 00:28:34 +02:00
}
2021-08-14 15:15:41 +02:00
return new RpcResult { req_msg_id = msgId , result = result } ;
2021-08-04 00:40:09 +02:00
}
2024-03-26 02:30:16 +01:00
private sealed class Rpc
2021-08-14 15:15:41 +02:00
{
2023-07-08 01:34:31 +02:00
internal Type type ;
internal TaskCompletionSource < object > tcs = new ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
internal long msgId ;
2022-03-27 22:29:48 +02:00
public Task < object > Task = > tcs . Task ;
}
private Rpc PullPendingRequest ( long msgId )
{
Rpc request ;
lock ( _pendingRpcs )
if ( _pendingRpcs . TryGetValue ( msgId , out request ) )
_pendingRpcs . Remove ( msgId ) ;
2021-08-14 15:15:41 +02:00
return request ;
}
2021-11-07 16:52:58 +01:00
private async Task HandleMessageAsync ( IObject obj )
2021-08-04 00:40:09 +02:00
{
2022-08-30 15:15:43 +02:00
if ( _bareRpc ! = null )
{
var rpc = PullPendingRequest ( _bareRpc . msgId ) ;
2022-11-08 17:06:16 +01:00
if ( ( rpc ? . type . IsAssignableFrom ( obj . GetType ( ) ) ) = = true )
{
_bareRpc = null ;
rpc . tcs . SetResult ( obj ) ;
return ;
}
2025-04-06 19:48:43 +02:00
else if ( _dcSession . authKeyID = = 0 )
2023-04-02 13:44:23 +02:00
throw new WTException ( $"Received a {obj.GetType()} incompatible with expected bare {rpc?.type}" ) ;
2022-11-08 17:06:16 +01:00
lock ( _pendingRpcs )
_pendingRpcs [ _bareRpc . msgId ] = _bareRpc ;
2022-08-30 15:15:43 +02:00
}
2021-08-04 00:40:09 +02:00
switch ( obj )
{
case MsgContainer container :
foreach ( var msg in container . messages )
2021-08-13 07:06:44 +02:00
if ( msg . body ! = null )
await HandleMessageAsync ( msg . body ) ;
2021-08-04 00:40:09 +02:00
break ;
2021-08-14 15:15:41 +02:00
case MsgCopy msgCopy :
if ( msgCopy ? . orig_message ? . body ! = null )
await HandleMessageAsync ( msgCopy . orig_message . body ) ;
break ;
2021-11-10 17:26:40 +01:00
case TL . Methods . Ping ping :
2021-11-07 16:50:59 +01:00
_ = SendAsync ( new Pong { msg_id = _lastRecvMsgId , ping_id = ping . ping_id } , false ) ;
2021-08-14 15:15:41 +02:00
break ;
case Pong pong :
2021-09-05 01:08:16 +02:00
SetResult ( pong . msg_id , pong ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( pong ) ;
2021-08-14 15:15:41 +02:00
break ;
case FutureSalts futureSalts :
2021-09-05 01:08:16 +02:00
SetResult ( futureSalts . req_msg_id , futureSalts ) ;
2021-08-04 00:40:09 +02:00
break ;
case RpcResult rpcResult :
2021-08-14 15:15:41 +02:00
break ; // SetResult was already done in ReadRpcResult
case MsgsAck msgsAck :
break ; // we don't do anything with these, for now
case BadMsgNotification badMsgNotification :
2021-12-04 00:14:15 +01:00
await _sendSemaphore . WaitAsync ( ) ;
2025-04-06 19:48:43 +02:00
bool retryLast = badMsgNotification . bad_msg_id = = _dcSession . lastSentMsgId ;
2021-12-04 00:14:15 +01:00
var lastSentMsg = _lastSentMsg ;
_sendSemaphore . Release ( ) ;
2021-12-21 02:28:35 +01:00
var logLevel = badMsgNotification . error_code = = 48 ? 2 : 4 ;
Helpers . Log ( logLevel , $"BadMsgNotification {badMsgNotification.error_code} for msg #{(short)badMsgNotification.bad_msg_id.GetHashCode():X4}" ) ;
2021-12-04 00:14:15 +01:00
switch ( badMsgNotification . error_code )
2021-08-27 22:44:43 +02:00
{
2022-01-23 01:28:10 +01:00
case 16 : // msg_id too low (most likely, client time is wrong; synchronize it using msg_id notifications and re-send the original message)
case 17 : // msg_id too high (similar to the previous case, the client time has to be synchronized, and the message re-sent with the correct msg_id)
2025-04-06 19:48:43 +02:00
_dcSession . lastSentMsgId = 0 ;
2021-12-30 23:45:28 +01:00
var localTime = DateTime . UtcNow ;
2025-04-06 19:48:43 +02:00
_dcSession . serverTicksOffset = ( _lastRecvMsgId > > 32 ) * 10000000 - localTime . Ticks + 621355968000000000L ;
Helpers . Log ( 1 , $"Time offset: {_dcSession.serverTicksOffset} | Server: {MsgIdToStamp(_lastRecvMsgId).AddTicks(_dcSession.serverTicksOffset).TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC" ) ;
2021-12-30 23:45:28 +01:00
break ;
2021-12-04 00:14:15 +01:00
case 32 : // msg_seqno too low (the server has already received a message with a lower msg_id but with either a higher or an equal and odd seqno)
case 33 : // msg_seqno too high (similarly, there is a message with a higher msg_id but with either a lower or an equal and odd seqno)
2025-04-06 19:48:43 +02:00
if ( _dcSession . seqno < = 1 )
2021-12-04 00:14:15 +01:00
retryLast = false ;
else
{
2024-09-07 18:43:52 +02:00
await ResetAsync ( false , false ) ;
2021-12-04 00:14:15 +01:00
_dcSession . Renew ( ) ;
await ConnectAsync ( ) ;
}
break ;
case 48 : // incorrect server salt (in this case, the bad_server_salt response is received with the correct salt, and the message is to be re-sent with it)
2023-12-18 00:01:07 +01:00
_dcSession . OldSalt = _dcSession . Salt ;
2023-05-02 22:49:38 +02:00
_dcSession . Salt = ( ( BadServerSalt ) badMsgNotification ) . new_server_salt ;
2025-04-06 19:48:43 +02:00
lock ( _session ) _session . Save ( ) ;
2023-05-02 22:49:38 +02:00
CheckSalt ( ) ;
2021-12-04 00:14:15 +01:00
break ;
default :
retryLast = false ;
break ;
}
if ( retryLast )
{
2022-03-27 22:29:48 +02:00
Rpc prevRequest ;
lock ( _pendingRpcs )
_pendingRpcs . TryGetValue ( badMsgNotification . bad_msg_id , out prevRequest ) ;
2023-07-06 10:17:29 +02:00
await SendAsync ( lastSentMsg , lastSentMsg is not MsgContainer , prevRequest ) ;
2022-03-27 22:29:48 +02:00
lock ( _pendingRpcs )
_pendingRpcs . Remove ( badMsgNotification . bad_msg_id ) ;
2021-08-27 22:44:43 +02:00
}
2022-03-27 22:29:48 +02:00
else if ( PullPendingRequest ( badMsgNotification . bad_msg_id ) is Rpc rpc )
2021-12-04 00:14:15 +01:00
{
2022-06-15 12:21:22 +02:00
if ( _bareRpc ? . msgId = = badMsgNotification . bad_msg_id ) _bareRpc = null ;
2023-04-02 13:44:23 +02:00
rpc . tcs . SetException ( new WTException ( $"BadMsgNotification {badMsgNotification.error_code}" ) ) ;
2021-12-04 00:14:15 +01:00
}
else
2024-03-29 16:42:58 +01:00
RaiseUpdates ( badMsgNotification ) ;
2021-08-27 22:44:43 +02:00
break ;
2021-08-04 00:40:09 +02:00
default :
2024-03-29 16:42:58 +01:00
RaiseUpdates ( obj ) ;
2021-08-04 00:40:09 +02:00
break ;
}
2021-08-14 15:15:41 +02:00
2021-09-05 01:08:16 +02:00
void SetResult ( long msgId , object result )
2021-08-14 15:15:41 +02:00
{
2022-03-27 22:29:48 +02:00
var rpc = PullPendingRequest ( msgId ) ;
if ( rpc ! = null )
rpc . tcs . SetResult ( result ) ;
2021-09-03 00:06:48 +02:00
else
2024-03-29 16:42:58 +01:00
RaiseUpdates ( obj ) ;
2021-09-05 01:08:16 +02:00
}
}
2024-03-29 16:42:58 +01:00
private async void RaiseUpdates ( IObject obj )
2021-09-05 01:08:16 +02:00
{
try
{
2024-03-29 16:42:58 +01:00
var task = obj is UpdatesBase updates ? OnUpdates ? . Invoke ( updates ) : OnOther ? . Invoke ( obj ) ;
2022-08-06 13:06:46 +02:00
if ( task ! = null ) await task ;
2021-09-05 01:08:16 +02:00
}
catch ( Exception ex )
{
2024-03-29 16:42:58 +01:00
Helpers . Log ( 4 , $"{nameof(OnUpdates)}({obj?.GetType().Name}) raised {ex}" ) ;
2021-08-14 15:15:41 +02:00
}
2021-08-04 00:40:09 +02:00
}
2024-11-22 16:27:21 +01:00
private void CheckRaiseOwnUpdates ( object result )
{
if ( OnOwnUpdates = = null ) return ;
if ( result is UpdatesBase updates )
RaiseOwnUpdates ( updates ) ;
else if ( result is Payments_PaymentResult ppr )
RaiseOwnUpdates ( ppr . updates ) ;
else if ( result is Messages_InvitedUsers miu )
RaiseOwnUpdates ( miu . updates ) ;
}
2024-03-29 16:42:58 +01:00
private async void RaiseOwnUpdates ( UpdatesBase updates )
2023-11-30 16:06:37 +01:00
{
try
{
2024-09-08 19:16:23 +02:00
await OnOwnUpdates ? . Invoke ( updates ) ;
2023-11-30 16:06:37 +01:00
}
catch ( Exception ex )
{
2024-03-29 16:42:58 +01:00
Helpers . Log ( 4 , $"{nameof(OnOwnUpdates)}({updates.GetType().Name}) raised {ex}" ) ;
2023-11-30 16:06:37 +01:00
}
}
2022-04-06 18:38:54 +02:00
static async Task < TcpClient > DefaultTcpHandler ( string host , int port )
2021-08-24 17:24:46 +02:00
{
2022-04-06 18:38:54 +02:00
var tcpClient = new TcpClient ( ) ;
2023-02-26 17:09:45 +01:00
await tcpClient . ConnectAsync ( host , port ) ;
2022-04-06 18:38:54 +02:00
return tcpClient ;
2021-08-24 17:24:46 +02:00
}
2024-02-19 17:08:26 +01:00
private IPEndPoint GetDefaultEndpoint ( out int dcId )
{
string addr = Config ( "server_address" ) ;
dcId = addr . Length > 2 & & addr [ 1 ] = = '>' & & addr [ 0 ] is > '0' and < = '9' ? addr [ 0 ] - '0' : 0 ;
return Compat . IPEndPoint_Parse ( dcId = = 0 ? addr : addr [ 2. . ] ) ;
}
2022-08-12 21:19:10 +02:00
/// <summary>Establish connection to Telegram servers without negociating a user session</summary>
2024-02-18 22:50:42 +01:00
/// <param name="quickResume">Attempt to resume session immediately without issuing Layer/InitConnection/GetConfig <i>(not recommended by default)</i></param>
2022-08-12 21:19:10 +02:00
/// <remarks>Usually you shouldn't need to call this method: Use <see cref="LoginUserIfNeeded">LoginUserIfNeeded</see> instead. <br/>Config callback is queried for: <b>server_address</b></remarks>
2022-04-06 18:38:54 +02:00
/// <returns>Most methods of this class are async (Task), so please use <see langword="await"/></returns>
2024-02-18 22:50:42 +01:00
public async Task ConnectAsync ( bool quickResume = false )
2021-08-04 00:40:09 +02:00
{
2022-04-06 18:38:54 +02:00
lock ( this )
2024-02-18 22:50:42 +01:00
_connecting ? ? = DoConnectAsync ( quickResume ) ;
2022-04-06 18:38:54 +02:00
await _connecting ;
}
2024-09-07 18:50:07 +02:00
private IEnumerable < DcOption > GetDcOptions ( int dcId , DcOption . Flags flags ) = > ! flags . HasFlag ( DcOption . Flags . media_only )
? _session . DcOptions . Where ( dc = > dc . id = = dcId & & ( dc . flags & ( DcOption . Flags . cdn | DcOption . Flags . tcpo_only | DcOption . Flags . media_only ) ) = = 0 )
. OrderBy ( dc = > dc . flags ^ flags )
: _session . DcOptions . Where ( dc = > dc . id = = dcId & & ( dc . flags & ( DcOption . Flags . cdn | DcOption . Flags . tcpo_only ) ) = = 0 )
. OrderBy ( dc = > ~ dc . flags & DcOption . Flags . media_only ) . ThenBy ( dc = > dc . flags ^ flags )
. Select ( dc = > dc . flags . HasFlag ( DcOption . Flags . media_only ) ? dc : new DcOption { id = dc . id , port = dc . port ,
ip_address = dc . ip_address , secret = dc . secret , flags = dc . flags | DcOption . Flags . media_only } ) ;
2024-02-18 22:50:42 +01:00
private async Task DoConnectAsync ( bool quickResume )
2022-04-06 18:38:54 +02:00
{
_cts = new ( ) ;
IPEndPoint endpoint = null ;
byte [ ] preamble , secret = null ;
int dcId = _dcSession ? . DcID ? ? 0 ;
if ( dcId = = 0 ) dcId = 2 ;
if ( MTProxyUrl ! = null )
2021-08-27 14:14:24 +02:00
{
2022-04-06 18:38:54 +02:00
#if OBFUSCATION
2025-06-26 22:02:26 +02:00
if ( TLConfig ? . test_mode = = true ) dcId + = dcId < 0 ? - 10000 : 10000 ;
2022-04-06 18:38:54 +02:00
var parms = HttpUtility . ParseQueryString ( MTProxyUrl [ MTProxyUrl . IndexOf ( '?' ) . . ] ) ;
var server = parms [ "server" ] ;
int port = int . Parse ( parms [ "port" ] ) ;
var str = parms [ "secret" ] ; // can be hex or base64
var secretBytes = secret = str . All ( "0123456789ABCDEFabcdef" . Contains ) ? Convert . FromHexString ( str ) :
System . Convert . FromBase64String ( str . Replace ( '_' , '/' ) . Replace ( '-' , '+' ) + new string ( '=' , ( 2147483644 - str . Length ) % 4 ) ) ;
var tlsMode = secret . Length > = 21 & & secret [ 0 ] = = 0xEE ;
if ( tlsMode | | ( secret . Length = = 17 & & secret [ 0 ] = = 0xDD ) )
2021-08-25 15:32:25 +02:00
{
2022-04-06 18:38:54 +02:00
_paddedMode = true ;
secret = secret [ 1. . 17 ] ;
2021-08-25 15:32:25 +02:00
}
2024-03-12 19:07:48 +01:00
else if ( secret . Length ! = 16 ) throw new ArgumentException ( "Invalid/unsupported secret" ) ;
2022-04-06 18:38:54 +02:00
Helpers . Log ( 2 , $"Connecting to DC {dcId} via MTProxy {server}:{port}..." ) ;
_tcpClient = await TcpHandler ( server , port ) ;
_networkStream = _tcpClient . GetStream ( ) ;
if ( tlsMode )
_networkStream = await TlsStream . HandshakeAsync ( _networkStream , secret , secretBytes [ 17. . ] , _cts . Token ) ;
#else
throw new Exception ( "Library was not compiled with OBFUSCATION symbol" ) ;
#endif
2021-08-06 07:28:54 +02:00
}
2024-10-25 01:35:17 +02:00
else if ( _httpClient ! = null )
2024-11-18 13:44:32 +01:00
{
Helpers . Log ( 2 , $"Using HTTP Mode" ) ;
2024-09-30 02:15:10 +02:00
_reactorTask = Task . CompletedTask ;
2024-11-18 13:44:32 +01:00
}
2022-04-06 18:38:54 +02:00
else
2021-08-06 07:28:54 +02:00
{
2024-02-19 17:08:26 +01:00
endpoint = _dcSession ? . EndPoint ? ? GetDefaultEndpoint ( out int defaultDc ) ;
2022-04-06 18:38:54 +02:00
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
TcpClient tcpClient = null ;
try
{
try
{
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
}
catch ( SocketException ex ) // cannot connect to target endpoint, try to find an alternate
{
Helpers . Log ( 4 , $"SocketException {ex.SocketErrorCode} ({ex.ErrorCode}): {ex.Message}" ) ;
if ( _dcSession ? . DataCenter = = null ) throw ;
var triedEndpoints = new HashSet < IPEndPoint > { endpoint } ;
if ( _session . DcOptions ! = null )
{
2024-09-07 18:50:07 +02:00
var altOptions = GetDcOptions ( _dcSession . DataCenter . id , _dcSession . DataCenter . flags ) ;
2022-04-06 18:38:54 +02:00
// try alternate addresses for this DC
foreach ( var dcOption in altOptions )
{
endpoint = new ( IPAddress . Parse ( dcOption . ip_address ) , dcOption . port ) ;
if ( ! triedEndpoints . Add ( endpoint ) ) continue ;
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
try
{
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
2024-09-07 18:50:07 +02:00
_dcSession . DataCenter = dcOption ;
2022-04-06 18:38:54 +02:00
break ;
}
catch ( SocketException ) { }
}
}
if ( tcpClient = = null )
{
2024-02-19 17:08:26 +01:00
endpoint = GetDefaultEndpoint ( out defaultDc ) ; // re-ask callback for an address
2022-04-06 18:38:54 +02:00
if ( ! triedEndpoints . Add ( endpoint ) ) throw ;
_dcSession . Client = null ;
// is it address for a known DCSession?
_dcSession = _session . DCSessions . Values . FirstOrDefault ( dcs = > dcs . EndPoint . Equals ( endpoint ) ) ;
2024-02-21 02:11:16 +01:00
if ( defaultDc ! = 0 ) _dcSession ? ? = _session . DCSessions . GetValueOrDefault ( defaultDc ) ;
2025-04-06 19:48:43 +02:00
_dcSession ? ? = new ( ) ;
2022-04-06 18:38:54 +02:00
_dcSession . Client = this ;
2024-05-28 00:37:58 +02:00
_dcSession . DataCenter = null ;
2022-04-06 18:38:54 +02:00
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
}
}
}
2022-05-09 23:43:06 +02:00
catch
2022-04-06 18:38:54 +02:00
{
tcpClient ? . Dispose ( ) ;
throw ;
}
_tcpClient = tcpClient ;
_networkStream = _tcpClient . GetStream ( ) ;
}
2024-09-30 02:15:10 +02:00
_dcSession . Salts ? . Remove ( DateTime . MaxValue ) ;
if ( _networkStream ! = null )
{
byte protocolId = ( byte ) ( _paddedMode ? 0xDD : 0xEE ) ;
2022-04-06 18:38:54 +02:00
#if OBFUSCATION
2024-09-30 02:15:10 +02:00
( _sendCtr , _recvCtr , preamble ) = InitObfuscation ( secret , protocolId , dcId ) ;
2022-04-06 18:38:54 +02:00
#else
2024-09-30 02:15:10 +02:00
preamble = new byte [ ] { protocolId , protocolId , protocolId , protocolId } ;
2022-04-06 18:38:54 +02:00
#endif
2024-09-30 02:15:10 +02:00
await _networkStream . WriteAsync ( preamble , 0 , preamble . Length , _cts . Token ) ;
2022-04-06 18:38:54 +02:00
2025-06-26 22:02:26 +02:00
_reactorTask = Reactor ( _networkStream , _cts . Token ) ;
2024-09-30 02:15:10 +02:00
}
2022-04-06 18:38:54 +02:00
_sendSemaphore . Release ( ) ;
try
{
2025-04-06 19:48:43 +02:00
if ( _dcSession . authKeyID = = 0 )
2022-04-06 18:38:54 +02:00
await CreateAuthorizationKey ( this , _dcSession ) ;
2024-09-30 02:15:10 +02:00
if ( _networkStream ! = null ) _ = KeepAlive ( _cts . Token ) ;
2024-02-18 22:50:42 +01:00
if ( quickResume & & _dcSession . Layer = = Layer . Version & & _dcSession . DataCenter ! = null & & _session . MainDC ! = 0 )
TLConfig = new Config { this_dc = _session . MainDC , dc_options = _session . DcOptions } ;
else
2022-04-06 18:38:54 +02:00
{
2024-06-15 17:35:25 +02:00
if ( _dcSession . Layer ! = 0 & & _dcSession . Layer ! = Layer . Version ) _dcSession . Renew ( ) ;
2025-03-12 02:17:36 +01:00
await InitConnection ( ) ;
2024-02-18 22:50:42 +01:00
if ( _dcSession . DataCenter = = null )
{
_dcSession . DataCenter = _session . DcOptions . Where ( dc = > dc . id = = TLConfig . this_dc )
. OrderByDescending ( dc = > dc . ip_address = = endpoint ? . Address . ToString ( ) )
. ThenByDescending ( dc = > dc . port = = endpoint ? . Port )
. ThenByDescending ( dc = > dc . flags = = ( endpoint ? . AddressFamily = = AddressFamily . InterNetworkV6 ? DcOption . Flags . ipv6 : 0 ) )
. First ( ) ;
_session . DCSessions [ TLConfig . this_dc ] = _dcSession ;
}
if ( _session . MainDC = = 0 ) _session . MainDC = TLConfig . this_dc ;
2022-04-06 18:38:54 +02:00
}
}
finally
{
2024-06-15 02:35:38 +02:00
if ( _reactorTask ! = null ) // client not disposed
lock ( _session ) _session . Save ( ) ;
2022-04-06 18:38:54 +02:00
}
2023-05-02 22:49:38 +02:00
Helpers . Log ( 2 , $"Connected to {(TLConfig.test_mode ? " Test DC " : " DC ")} {TLConfig.this_dc}... {TLConfig.flags & (Config.Flags)~0x18E00U}" ) ;
2022-04-06 18:38:54 +02:00
}
2025-03-12 02:17:36 +01:00
private async Task InitConnection ( )
{
var initParams = JSONValue . FromJsonElement ( System . Text . Json . JsonDocument . Parse ( Config ( "init_params" ) ) . RootElement ) ;
TLConfig = await this . InvokeWithLayer ( Layer . Version ,
new TL . Methods . InitConnection < Config >
{
flags = TL . Methods . InitConnection < Config > . Flags . has_params ,
api_id = _session . 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" ) ,
params_ = initParams ,
query = new TL . Methods . Help_GetConfig ( )
} ) ;
_dcSession . Layer = Layer . Version ;
_session . DcOptions = TLConfig . dc_options ;
}
2022-04-06 18:38:54 +02:00
private async Task KeepAlive ( CancellationToken ct )
{
int ping_id = _random . Next ( ) ;
while ( ! ct . IsCancellationRequested )
{
await Task . Delay ( Math . Abs ( PingInterval ) * 1000 , ct ) ;
if ( PingInterval < = 0 )
await this . Ping ( ping_id + + ) ;
else // see https://core.telegram.org/api/optimisation#grouping-updates
#if DEBUG
await this . PingDelayDisconnect ( ping_id + + , PingInterval * 5 ) ;
#else
await this . PingDelayDisconnect ( ping_id + + , PingInterval * 5 / 4 ) ;
#endif
}
}
2022-09-20 17:30:32 +02:00
/// <summary>Login as a user with given phone number (or resume previous session)<br/>Call this method again to provide additional requested login information</summary>
/// <param name="loginInfo">First call should be with phone number<br/>Further calls should be with the requested configuration value</param>
/// <returns>Configuration item requested to continue login, or <see langword="null"/> when login is successful<br/>
2022-10-12 23:25:15 +02:00
/// Possible values: <b>verification_code</b>, <b>name</b> (signup), <b>password</b> (2FA), <b>email</b> & <b>email_verification_code</b> (email registration)</returns>
2023-04-02 13:44:23 +02:00
/// <exception cref="WTException"/><exception cref="RpcException"/>
2022-09-20 17:30:32 +02:00
public async Task < string > Login ( string loginInfo )
{
if ( _loginCfg . request = = default ) RunLoginAsync ( loginInfo ) ;
else
{
if ( await _loginCfg . request . Task = = null ) return null ;
loginInfo ? ? = AskConfig ( await _loginCfg . request . Task ) ;
_loginCfg . request = new ( ) ;
_loginCfg . response . SetResult ( loginInfo ) ;
}
return await _loginCfg . request . Task ;
}
private ( TaskCompletionSource < string > request , TaskCompletionSource < string > response ) _loginCfg ;
private async void RunLoginAsync ( string phone )
{
_loginCfg . request = new ( ) ;
var prevConfig = _config ;
_config = what = >
{
if ( prevConfig ( what ) is string value ) return value ;
switch ( what )
{
case "phone_number" : return phone ;
case "last_name" : break ;
case "first_name" : what = "name" ; goto case "email" ;
case "email" : case "email_verification_code" :
case "password" : case "verification_code" : _loginCfg . response = new ( ) ; _loginCfg . request . SetResult ( what ) ; break ;
default : return null ;
} ;
value = _loginCfg . response . Task . Result ;
if ( what = = "name" & & value ! = null )
{
var lf = value . IndexOf ( '\n' ) ;
if ( lf < 0 ) lf = value . IndexOf ( ' ' ) ;
_loginCfg . response = new ( ) ;
_loginCfg . response . SetResult ( lf < 0 ? "" : value [ ( lf + 1 ) . . ] ) ;
value = lf < 0 ? value : value [ 0. . lf ] ;
return value ;
}
return value ;
} ;
try
{
2024-09-06 18:22:05 +02:00
await ConnectAsync ( ) ; // start reactor on the current (UI?) context
2022-09-20 17:30:32 +02:00
// Login logic is executed on TaskScheduler while request TCS are still received on current SynchronizationContext
await Task . Run ( ( ) = > LoginUserIfNeeded ( ) ) ;
_loginCfg . request . SetResult ( null ) ;
}
catch ( Exception ex )
{
_loginCfg . request . SetException ( ex ) ;
}
finally
{
_config = prevConfig ;
}
}
2022-04-06 18:38:54 +02:00
/// <summary>Login as a bot (if not already logged-in).</summary>
2022-09-20 17:30:32 +02:00
/// <param name="bot_token">bot token, or <see langword="null"/> if token is provided by Config callback</param>
/// <remarks>Config callback may be queried for: <b>bot_token</b>
2022-04-06 18:38:54 +02:00
/// <br/>Bots can only call API methods marked with [bots: ✓] in their documentation. </remarks>
/// <returns>Detail about the logged-in bot</returns>
2022-09-20 17:30:32 +02:00
public async Task < User > LoginBotIfNeeded ( string bot_token = null )
2022-04-06 18:38:54 +02:00
{
await ConnectAsync ( ) ;
2022-09-20 17:30:32 +02:00
string botToken = bot_token ? ? Config ( "bot_token" ) ;
2022-04-06 18:38:54 +02:00
if ( _session . UserId ! = 0 ) // a user is already logged-in
{
try
{
2022-09-14 18:22:52 +02:00
var users = await this . Users_GetUsers ( InputUser . Self ) ; // this calls also reenable incoming Updates
2022-04-06 18:38:54 +02:00
var self = users [ 0 ] as User ;
if ( self . id = = long . Parse ( botToken . Split ( ':' ) [ 0 ] ) )
{
_session . UserId = _dcSession . UserId = self . id ;
2025-04-06 19:48:43 +02:00
lock ( _session ) _session . Save ( ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( self ) ;
2022-09-20 17:30:32 +02:00
return User = self ;
2022-04-06 18:38:54 +02:00
}
Helpers . Log ( 3 , $"Current logged user {self.id} mismatched bot_token. Logging out and in..." ) ;
}
catch ( Exception ex )
{
Helpers . Log ( 4 , $"Error while verifying current bot! ({ex.Message}) Proceeding to login..." ) ;
}
await this . Auth_LogOut ( ) ;
_session . UserId = _dcSession . UserId = 0 ;
2022-09-20 17:30:32 +02:00
User = null ;
2022-04-06 18:38:54 +02:00
}
var authorization = await this . Auth_ImportBotAuthorization ( 0 , _session . ApiId , _apiHash ? ? = Config ( "api_hash" ) , botToken ) ;
return LoginAlreadyDone ( authorization ) ;
}
/// <summary>Login as a user (if not already logged-in).
2022-08-12 21:19:10 +02:00
/// <br/><i>(this method calls <see cref="ConnectAsync">ConnectAsync</see> if necessary)</i></summary>
2022-10-12 23:25:15 +02:00
/// <remarks>Config callback is queried for: <b>phone_number</b>, <b>verification_code</b> <br/>and eventually <b>first_name</b>, <b>last_name</b> (signup required), <b>password</b> (2FA auth), <b>email</b> & <b>email_verification_code</b> (email registration), <b>user_id</b> (alt validation)</remarks>
2022-04-06 18:38:54 +02:00
/// <param name="settings">(optional) Preference for verification_code sending</param>
2022-07-08 01:15:37 +02:00
/// <param name="reloginOnFailedResume">Proceed to logout and login if active user session cannot be resumed successfully</param>
2022-04-06 18:38:54 +02:00
/// <returns>Detail about the logged-in user
/// <br/>Most methods of this class are async (Task), so please use <see langword="await"/> to get the result</returns>
2022-07-08 01:15:37 +02:00
public async Task < User > LoginUserIfNeeded ( CodeSettings settings = null , bool reloginOnFailedResume = true )
2022-04-06 18:38:54 +02:00
{
await ConnectAsync ( ) ;
string phone_number = null ;
if ( _session . UserId ! = 0 ) // a user is already logged-in
{
try
{
2022-09-14 18:22:52 +02:00
var users = await this . Users_GetUsers ( InputUser . Self ) ; // this call also reenable incoming Updates
2022-04-06 18:38:54 +02:00
var self = users [ 0 ] as User ;
// check user_id or phone_number match currently logged-in user
if ( ( long . TryParse ( _config ( "user_id" ) , out long id ) & & ( id = = - 1 | | self . id = = id ) ) | |
self . phone = = string . Concat ( ( phone_number = Config ( "phone_number" ) ) . Where ( char . IsDigit ) ) )
{
_session . UserId = _dcSession . UserId = self . id ;
2025-04-06 19:48:43 +02:00
lock ( _session ) _session . Save ( ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( self ) ;
2022-09-20 17:30:32 +02:00
return User = self ;
2022-04-06 18:38:54 +02:00
}
2022-07-08 01:15:37 +02:00
var mismatch = $"Current logged user {self.id} mismatched user_id or phone_number" ;
Helpers . Log ( 3 , mismatch ) ;
2023-04-02 13:44:23 +02:00
if ( ! reloginOnFailedResume ) throw new WTException ( mismatch ) ;
2022-04-06 18:38:54 +02:00
}
2022-07-08 01:15:37 +02:00
catch ( RpcException ex ) when ( reloginOnFailedResume )
2022-04-06 18:38:54 +02:00
{
2022-07-08 01:15:37 +02:00
Helpers . Log ( 4 , $"Error while fetching current user! ({ex.Message})" ) ;
2022-04-06 18:38:54 +02:00
}
2022-07-08 01:15:37 +02:00
Helpers . Log ( 3 , $"Proceeding to logout and login..." ) ;
2022-04-06 18:38:54 +02:00
await this . Auth_LogOut ( ) ;
_session . UserId = _dcSession . UserId = 0 ;
2022-09-20 17:30:32 +02:00
User = null ;
2022-04-06 18:38:54 +02:00
}
phone_number ? ? = Config ( "phone_number" ) ;
2023-02-04 10:36:19 +01:00
Auth_SentCodeBase sentCodeBase ;
2022-04-22 23:07:43 +02:00
#pragma warning disable CS0618 // Auth_* methods are marked as obsolete
2022-04-06 18:38:54 +02:00
try
{
2023-02-04 10:36:19 +01:00
sentCodeBase = await this . Auth_SendCode ( phone_number , _session . ApiId , _apiHash ? ? = Config ( "api_hash" ) , settings ? ? = new ( ) ) ;
2022-04-06 18:38:54 +02:00
}
catch ( RpcException ex ) when ( ex . Code = = 500 & & ex . Message = = "AUTH_RESTART" )
{
2023-02-04 10:36:19 +01:00
sentCodeBase = await this . Auth_SendCode ( phone_number , _session . ApiId , _apiHash , settings ) ;
2022-04-06 18:38:54 +02:00
}
2021-12-05 07:21:30 +01:00
Auth_AuthorizationBase authorization = null ;
2024-04-17 17:33:48 +02:00
string phone_code_hash = null , email = null ;
2022-04-22 23:07:43 +02:00
try
{
2023-02-04 10:36:19 +01:00
if ( sentCodeBase is Auth_SentCode { type : Auth_SentCodeTypeSetUpEmailRequired setupEmail } setupSentCode )
2022-09-14 18:29:07 +02:00
{
2023-02-04 10:36:19 +01:00
phone_code_hash = setupSentCode . phone_code_hash ;
2022-09-14 18:29:07 +02:00
Helpers . Log ( 3 , "A login email is required" ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( sentCodeBase ) ;
2024-04-17 17:33:48 +02:00
email = _config ( "email" ) ;
2022-09-14 18:29:07 +02:00
if ( string . IsNullOrEmpty ( email ) )
2023-02-04 10:36:19 +01:00
sentCodeBase = await this . Auth_ResendCode ( phone_number , phone_code_hash ) ;
2022-09-14 18:29:07 +02:00
else
{
2023-02-04 10:36:19 +01:00
var purpose = new EmailVerifyPurposeLoginSetup { phone_number = phone_number , phone_code_hash = phone_code_hash } ;
2022-09-14 18:29:07 +02:00
if ( email is not "Google" and not "Apple" )
{
var sentEmail = await this . Account_SendVerifyEmailCode ( purpose , email ) ;
Helpers . Log ( 3 , "An email verification code has been sent to " + sentEmail . email_pattern ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( sentEmail ) ;
2022-09-14 18:29:07 +02:00
}
Account_EmailVerified verified = null ;
for ( int retry = 1 ; verified = = null ; retry + + )
try
{
var code = await ConfigAsync ( "email_verification_code" ) ;
2024-04-17 17:33:48 +02:00
verified = await this . Account_VerifyEmail ( purpose , EmailVerification ( email , code ) ) ;
2022-09-14 18:29:07 +02:00
}
catch ( RpcException e ) when ( e . Code = = 400 & & e . Message is "CODE_INVALID" or "EMAIL_TOKEN_INVALID" )
{
Helpers . Log ( 4 , "Wrong email verification code!" ) ;
2022-10-20 23:12:43 +02:00
if ( retry > = MaxCodePwdAttempts ) throw ;
2022-09-14 18:29:07 +02:00
}
if ( verified is Account_EmailVerifiedLogin verifiedLogin ) // (it should always be)
2023-02-04 10:36:19 +01:00
sentCodeBase = verifiedLogin . sent_code ;
2022-09-14 18:29:07 +02:00
}
}
2022-04-22 23:07:43 +02:00
resent :
2023-02-04 10:36:19 +01:00
if ( sentCodeBase is Auth_SentCodeSuccess success )
authorization = success . authorization ;
else if ( sentCodeBase is Auth_SentCode sentCode )
{
phone_code_hash = sentCode . phone_code_hash ;
2023-02-13 11:29:02 +01:00
var timeout = DateTime . UtcNow + TimeSpan . FromSeconds ( sentCode . timeout ) ;
Helpers . Log ( 3 , $"A verification code has been sent via {sentCode.type.GetType().Name[17..]}" ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( sentCode ) ;
2023-02-05 15:43:49 +01:00
if ( sentCode . type is Auth_SentCodeTypeFirebaseSms firebaseSms )
{
2023-02-13 11:29:02 +01:00
var token = await ConfigAsync ( "firebase" ) ;
2023-02-05 15:43:49 +01:00
int index = token ? . IndexOf ( ':' ) ? ? - 1 ;
if ( ! ( index > 0 & & token [ . . index ] switch
{
"safety_net_token" = > await this . Auth_RequestFirebaseSms ( phone_number , phone_code_hash , safety_net_token : token [ ( index + 1 ) . . ] ) ,
"ios_push_secret" = > await this . Auth_RequestFirebaseSms ( phone_number , phone_code_hash , ios_push_secret : token [ ( index + 1 ) . . ] ) ,
_ = > false
} ) )
{
sentCodeBase = await this . Auth_ResendCode ( phone_number , phone_code_hash ) ;
goto resent ;
}
}
2023-02-04 10:36:19 +01:00
for ( int retry = 1 ; authorization = = null ; retry + + )
try
2021-12-13 15:28:06 +01:00
{
2023-02-04 10:36:19 +01:00
var verification_code = await ConfigAsync ( "verification_code" ) ;
if ( verification_code = = "" & & sentCode . next_type ! = 0 )
2022-04-22 23:07:43 +02:00
{
2023-02-04 10:36:19 +01:00
var mustWait = timeout - DateTime . UtcNow ;
if ( mustWait . Ticks > 0 )
{
Helpers . Log ( 3 , $"You must wait {(int)(mustWait.TotalSeconds + 0.5)} more seconds before requesting the code to be sent via {sentCode.next_type}" ) ;
continue ;
}
sentCodeBase = await this . Auth_ResendCode ( phone_number , phone_code_hash ) ;
goto resent ;
2022-04-22 23:07:43 +02:00
}
2024-04-17 17:33:48 +02:00
if ( sentCode . type is Auth_SentCodeTypeEmailCode )
authorization = await this . Auth_SignIn ( phone_number , phone_code_hash , null , EmailVerification ( email ? ? = _config ( "email" ) , verification_code ) ) ;
else
authorization = await this . Auth_SignIn ( phone_number , phone_code_hash , verification_code ) ;
2021-12-13 15:28:06 +01:00
}
2023-02-04 10:36:19 +01:00
catch ( RpcException e ) when ( e . Code = = 400 & & e . Message = = "PHONE_CODE_INVALID" )
{
Helpers . Log ( 4 , "Wrong verification code!" ) ;
if ( retry > = MaxCodePwdAttempts ) throw ;
}
catch ( RpcException e ) when ( e . Code = = 401 & & e . Message = = "SESSION_PASSWORD_NEEDED" )
{
2024-04-27 12:34:32 +02:00
authorization = await LoginPasswordNeeded ( ) ;
2023-02-04 10:36:19 +01:00
}
}
2024-04-17 17:33:48 +02:00
static EmailVerification EmailVerification ( string email , string code ) = > email switch
{
"Google" = > new EmailVerificationGoogle { token = code } ,
"Apple" = > new EmailVerificationApple { token = code } ,
_ = > new EmailVerificationCode { code = code }
} ;
2022-04-22 23:07:43 +02:00
}
2022-10-10 21:55:56 +02:00
catch ( Exception ex ) when ( ex is not RpcException { Message : "FLOOD_WAIT_X" } )
2022-04-22 23:07:43 +02:00
{
2023-02-04 10:36:19 +01:00
try { await this . Auth_CancelCode ( phone_number , phone_code_hash ) ; } catch { }
2022-04-22 23:07:43 +02:00
throw ;
}
2021-08-06 07:28:54 +02:00
if ( authorization is Auth_AuthorizationSignUpRequired signUpRequired )
{
2021-08-06 20:17:19 +02:00
var waitUntil = DateTime . UtcNow . AddSeconds ( 3 ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( signUpRequired ) ; // give caller the possibility to read and accept TOS
2021-08-09 11:41:50 +02:00
var first_name = Config ( "first_name" ) ;
var last_name = Config ( "last_name" ) ;
2021-08-06 20:17:19 +02:00
var wait = waitUntil - DateTime . UtcNow ;
if ( wait > TimeSpan . Zero ) await Task . Delay ( wait ) ; // we get a FLOOD_WAIT_3 if we SignUp too fast
2023-02-04 10:36:19 +01:00
authorization = await this . Auth_SignUp ( phone_number , phone_code_hash , first_name , last_name ) ;
2021-08-06 07:28:54 +02:00
}
2022-04-22 23:07:43 +02:00
#pragma warning restore CS0618
2022-10-13 10:07:46 +02:00
LoginAlreadyDone ( authorization ) ;
if ( User . phone ! = string . Concat ( phone_number . Where ( char . IsDigit ) ) )
Helpers . Log ( 3 , $"Mismatched phone_number (+{User.phone} != {phone_number}). Fix this to avoid verification code each time" ) ;
return User ;
2021-12-05 07:21:30 +01:00
}
2024-04-27 12:34:32 +02:00
/// <summary>Login via QR code</summary>
/// <param name="qrDisplay">Callback to display the login url as QR code to the user (<a href="https://www.nuget.org/packages/QRCoder/">QRCoder library</a> can help you)</param>
/// <param name="except_ids">(optional) To prevent logging as these user ids</param>
/// <param name="logoutFirst">If session is already connected to a user, this method will first log out.<br/>You can also check property <see cref="UserId"/> before calling this method.</param>
/// <param name="ct">If you need to abort the method before login is completed</param>
/// <returns>Detail about the logged-in user</returns>
public async Task < User > LoginWithQRCode ( Action < string > qrDisplay , long [ ] except_ids = null , bool logoutFirst = true , CancellationToken ct = default )
{
await ConnectAsync ( ) ;
if ( logoutFirst & & _session . UserId ! = 0 ) // a user is already logged-in
{
await this . Auth_LogOut ( ) ;
_session . UserId = _dcSession . UserId = 0 ;
User = null ;
}
var tcs = new TaskCompletionSource < bool > ( ) ;
OnUpdates + = CatchQRUpdate ;
try
{
while ( ! ct . IsCancellationRequested )
{
var ltb = await this . Auth_ExportLoginToken ( _session . ApiId , _apiHash ? ? = Config ( "api_hash" ) , except_ids ) ;
retry :
switch ( ltb )
{
case Auth_LoginToken lt :
var url = "tg://login?token=" + System . Convert . ToBase64String ( lt . token ) . Replace ( '/' , '_' ) . Replace ( '+' , '-' ) ;
Helpers . Log ( 3 , $"Waiting for this QR code login to be accepted: " + url ) ;
qrDisplay ( url ) ;
if ( lt . expires - DateTime . UtcNow is { Ticks : > = 0 } delay )
await Task . WhenAny ( Task . Delay ( delay , ct ) , tcs . Task ) ;
break ;
case Auth_LoginTokenMigrateTo ltmt :
await MigrateToDC ( ltmt . dc_id ) ;
ltb = await this . Auth_ImportLoginToken ( ltmt . token ) ;
goto retry ;
case Auth_LoginTokenSuccess lts :
return LoginAlreadyDone ( lts . authorization ) ;
}
}
ct . ThrowIfCancellationRequested ( ) ;
return null ;
}
catch ( RpcException e ) when ( e . Code = = 401 & & e . Message = = "SESSION_PASSWORD_NEEDED" )
{
return LoginAlreadyDone ( await LoginPasswordNeeded ( ) ) ;
}
finally
{
OnUpdates - = CatchQRUpdate ;
}
Task CatchQRUpdate ( UpdatesBase updates )
{
if ( updates . UpdateList . OfType < UpdateLoginToken > ( ) . Any ( ) )
tcs . TrySetResult ( true ) ;
return Task . CompletedTask ;
}
}
private async Task < Auth_AuthorizationBase > LoginPasswordNeeded ( )
{
for ( int pwdRetry = 1 ; ; pwdRetry + + )
try
{
var accountPassword = await this . Account_GetPassword ( ) ;
RaiseUpdates ( accountPassword ) ;
var checkPasswordSRP = await Check2FA ( accountPassword , ( ) = > ConfigAsync ( "password" ) ) ;
return await this . Auth_CheckPassword ( checkPasswordSRP ) ;
}
catch ( RpcException pe ) when ( pe . Code = = 400 & & pe . Message = = "PASSWORD_HASH_INVALID" )
{
Helpers . Log ( 4 , "Wrong password!" ) ;
if ( pwdRetry > = MaxCodePwdAttempts ) throw ;
}
}
2021-12-05 07:21:30 +01:00
/// <summary><b>[Not recommended]</b> You can use this if you have already obtained a login authorization manually</summary>
/// <param name="authorization">if this was not a successful Auth_Authorization, an exception is thrown</param>
/// <returns>the User that was authorized</returns>
/// <remarks>This approach is not recommended because you likely didn't properly handle all aspects of the login process
/// <br/>(transient failures, unnecessary login, 2FA, sign-up required, slowness to respond, verification code resending, encryption key safety, etc..)
/// <br/>Methods <c>LoginUserIfNeeded</c> and <c>LoginBotIfNeeded</c> handle these automatically for you</remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public User LoginAlreadyDone ( Auth_AuthorizationBase authorization )
{
2024-03-13 05:03:15 +01:00
if ( authorization is not Auth_Authorization { user : User self } )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Failed to get Authorization: " + authorization . GetType ( ) . Name ) ;
2024-03-13 05:03:15 +01:00
_session . UserId = _dcSession . UserId = self . id ;
2022-01-27 17:34:16 +01:00
lock ( _session ) _session . Save ( ) ;
2024-03-29 16:42:58 +01:00
RaiseUpdates ( self ) ;
2024-03-13 05:03:15 +01:00
return User = self ;
2021-08-04 00:40:09 +02:00
}
2021-08-05 16:29:58 +02:00
2022-04-06 18:38:54 +02:00
private MsgsAck CheckMsgsToAck ( )
2021-08-05 16:29:58 +02:00
{
2022-04-06 18:38:54 +02:00
lock ( _msgsToAck )
2021-08-05 16:29:58 +02:00
{
2022-04-06 18:38:54 +02:00
if ( _msgsToAck . Count = = 0 ) return null ;
2024-03-08 12:07:37 +01:00
var msgsAck = new MsgsAck { msg_ids = [ . . _msgsToAck ] } ;
2022-04-06 18:38:54 +02:00
_msgsToAck . Clear ( ) ;
return msgsAck ;
2021-08-05 16:29:58 +02:00
}
}
2022-04-06 18:38:54 +02:00
internal ( long msgId , int seqno ) NewMsgId ( bool isContent )
2021-08-05 16:29:58 +02:00
{
2022-04-06 18:38:54 +02:00
int seqno ;
2025-04-06 19:48:43 +02:00
long msgId = DateTime . UtcNow . Ticks + _dcSession . serverTicksOffset - 621355968000000000L ;
2022-04-06 18:38:54 +02:00
msgId = msgId * 428 + ( msgId > > 24 ) * 25110956 ; // approximately unixtime*2^32 and divisible by 4
2025-06-24 19:01:52 +02:00
lock ( _session )
{
if ( msgId < = _dcSession . lastSentMsgId ) msgId = _dcSession . lastSentMsgId + = 4 ; else _dcSession . lastSentMsgId = msgId ;
seqno = isContent ? _dcSession . seqno + + * 2 + 1 : _dcSession . seqno * 2 ;
}
2022-04-06 18:38:54 +02:00
return ( msgId , seqno ) ;
2021-08-05 16:29:58 +02:00
}
2022-04-06 18:38:54 +02:00
private async Task SendAsync ( IObject msg , bool isContent , Rpc rpc = null )
2021-08-05 16:29:58 +02:00
{
2023-04-02 13:44:23 +02:00
if ( _reactorTask = = null ) throw new WTException ( "You must connect to Telegram first" ) ;
2025-04-06 19:48:43 +02:00
isContent & = _dcSession . authKeyID ! = 0 ;
2024-10-07 02:43:07 +02:00
var ( msgId , seqno ) = NewMsgId ( isContent ) ;
2022-04-06 18:38:54 +02:00
if ( rpc ! = null )
lock ( _pendingRpcs )
_pendingRpcs [ rpc . msgId = msgId ] = rpc ;
2024-10-07 02:43:07 +02:00
if ( isContent )
2021-11-09 01:43:27 +01:00
{
2024-10-07 02:43:07 +02:00
List < _Message > messages = null ;
if ( _httpWait ! = null & & NewMsgId ( false ) is var ( hwId , hwSeqno ) )
( messages ? ? = [ ] ) . Add ( new ( hwId , hwSeqno , _httpWait ) ) ;
if ( CheckMsgsToAck ( ) is MsgsAck msgsAck & & NewMsgId ( false ) is var ( ackId , ackSeqno ) )
( messages ? ? = [ ] ) . Add ( new ( ackId , ackSeqno , msgsAck ) ) ;
if ( messages ! = null )
{
messages . Add ( new ( msgId , seqno , msg ) ) ;
await SendAsync ( new MsgContainer { messages = messages } , false ) ;
return ;
}
2021-11-09 01:43:27 +01:00
}
2024-11-18 13:44:32 +01:00
Task receiveTask = null ;
var sem = _sendSemaphore ;
await sem . WaitAsync ( _cts . Token ) ;
2022-04-06 18:38:54 +02:00
try
2021-11-09 01:43:27 +01:00
{
2022-04-06 18:38:54 +02:00
using var memStream = new MemoryStream ( 1024 ) ;
2022-10-04 00:52:44 +02:00
using var writer = new BinaryWriter ( memStream ) ;
2022-04-06 18:38:54 +02:00
writer . Write ( 0 ) ; // int32 payload_len (to be patched with payload length)
2021-09-23 13:13:36 +02:00
2025-04-06 19:48:43 +02:00
if ( _dcSession . authKeyID = = 0 ) // send unencrypted message
2022-01-20 02:44:43 +01:00
{
2023-04-02 13:44:23 +02:00
if ( _bareRpc = = null ) throw new WTException ( $"Shouldn't send a {msg.GetType().Name} unencrypted" ) ;
2022-09-19 22:28:12 +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)
2022-04-06 18:38:54 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_')}..." ) ;
2022-09-19 22:28:12 +02:00
writer . WriteTLObject ( msg ) ; // bytes message_data
2022-04-06 18:38:54 +02:00
BinaryPrimitives . WriteInt32LittleEndian ( memStream . GetBuffer ( ) . AsSpan ( 20 ) , ( int ) memStream . Length - 24 ) ; // patch message_data_length
2022-01-20 02:44:43 +01:00
}
2022-04-06 18:38:54 +02:00
else
{
2023-05-02 22:49:38 +02:00
CheckSalt ( ) ;
2022-04-06 18:38:54 +02:00
using var clearStream = new MemoryStream ( 1024 ) ;
2022-10-04 00:52:44 +02:00
using var clearWriter = new BinaryWriter ( clearStream ) ;
2022-04-06 18:38:54 +02:00
clearWriter . Write ( _dcSession . AuthKey , 88 , 32 ) ;
2022-09-19 22:28:12 +02:00
clearWriter . Write ( _dcSession . Salt ) ; // int64 salt
2025-04-06 19:48:43 +02:00
clearWriter . Write ( _dcSession . id ) ; // int64 session_id
2022-09-19 22:28:12 +02:00
clearWriter . Write ( msgId ) ; // int64 message_id
clearWriter . Write ( seqno ) ; // int32 msg_seqno
clearWriter . Write ( 0 ) ; // int32 message_data_length (to be patched)
2022-04-06 18:38:54 +02:00
if ( ( seqno & 1 ) ! = 0 )
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} #{(short)msgId.GetHashCode():X4}" ) ;
else
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} {MsgIdToStamp(msgId):u} (svc)" ) ;
2022-09-19 22:28:12 +02:00
clearWriter . WriteTLObject ( msg ) ; // bytes message_data
2022-04-06 18:38:54 +02:00
int clearLength = ( int ) clearStream . Length - 32 ; // length before padding (= 32 + message_data_length)
int padding = ( 0x7FFFFFF0 - clearLength ) % 16 ;
2022-10-04 00:52:44 +02:00
padding + = _random . Next ( 2 , 16 ) * 16 ; // MTProto 2.0 padding must be between 12..1024 with total length divisible by 16
2022-04-06 18:38:54 +02:00
clearStream . SetLength ( 32 + clearLength + padding ) ;
byte [ ] clearBuffer = clearStream . GetBuffer ( ) ;
BinaryPrimitives . WriteInt32LittleEndian ( clearBuffer . AsSpan ( 60 ) , clearLength - 32 ) ; // patch message_data_length
RNG . GetBytes ( clearBuffer , 32 + clearLength , padding ) ;
var msgKeyLarge = _sha256 . ComputeHash ( clearBuffer , 0 , 32 + clearLength + padding ) ;
const int msgKeyOffset = 8 ; // msg_key = middle 128-bits of SHA256(authkey_part+plaintext+padding)
2022-10-01 13:56:43 +02:00
byte [ ] encrypted_data = EncryptDecryptMessage ( clearBuffer . AsSpan ( 32 , clearLength + padding ) , true , 0 , _dcSession . AuthKey , msgKeyLarge , msgKeyOffset , _sha256 ) ;
2022-01-20 02:44:43 +01:00
2025-04-06 19:48:43 +02:00
writer . Write ( _dcSession . authKeyID ) ; // int64 auth_key_id
2022-09-19 22:28:12 +02:00
writer . Write ( msgKeyLarge , msgKeyOffset , 16 ) ; // int128 msg_key
writer . Write ( encrypted_data ) ; // bytes encrypted_data
2022-04-06 18:38:54 +02:00
}
if ( _paddedMode ) // Padded intermediate mode => append random padding
2022-01-20 02:44:43 +01:00
{
2025-04-06 19:48:43 +02:00
var padding = new byte [ _random . Next ( _dcSession . authKeyID = = 0 ? 257 : 16 ) ] ;
2022-04-06 18:38:54 +02:00
RNG . GetBytes ( padding ) ;
writer . Write ( padding ) ;
2022-01-20 02:44:43 +01:00
}
2022-04-06 18:38:54 +02:00
var buffer = memStream . GetBuffer ( ) ;
int frameLength = ( int ) memStream . Length ;
BinaryPrimitives . WriteInt32LittleEndian ( buffer , frameLength - 4 ) ; // patch payload_len with correct value
#if OBFUSCATION
2024-09-30 02:15:10 +02:00
_sendCtr ? . EncryptDecrypt ( buffer , frameLength ) ;
2022-04-06 18:38:54 +02:00
#endif
2024-11-18 13:44:32 +01:00
if ( _networkStream ! = null )
await _networkStream . WriteAsync ( buffer , 0 , frameLength ) ;
else
receiveTask = SendReceiveHttp ( buffer , frameLength ) ;
2022-04-06 18:38:54 +02:00
_lastSentMsg = msg ;
}
finally
{
2024-11-18 13:44:32 +01:00
sem . Release ( ) ;
2022-01-20 02:44:43 +01:00
}
2024-11-18 13:44:32 +01:00
if ( receiveTask ! = null ) await receiveTask ;
2021-09-23 13:13:36 +02:00
}
2024-11-18 13:44:32 +01:00
private async Task SendReceiveHttp ( byte [ ] buffer , int frameLength )
2024-09-30 02:15:10 +02:00
{
2024-11-18 13:44:32 +01:00
var endpoint = _dcSession ? . EndPoint ? ? GetDefaultEndpoint ( out _ ) ;
var content = new ByteArrayContent ( buffer , 4 , frameLength - 4 ) ;
var response = await _httpClient . PostAsync ( $"http://{endpoint}/api" , content , _cts . Token ) ;
if ( response . StatusCode ! = HttpStatusCode . OK )
throw new RpcException ( ( int ) response . StatusCode , TransportError ( ( int ) response . StatusCode ) ) ;
var data = await response . Content . ReadAsByteArrayAsync ( ) ;
var obj = ReadFrame ( data , data . Length ) ;
if ( obj ! = null )
await HandleMessageAsync ( obj ) ;
2024-09-30 02:15:10 +02:00
}
2024-10-07 02:43:07 +02:00
/// <summary>Long poll on HTTP connections</summary>
/// <param name="httpWait">Parameters for the long poll. Leave <see langword="null"/> for the default 25 seconds.</param>
/// <remarks>⚠️ Telegram servers don't seem to support other parameter than <see langword="null"/> correctly</remarks>
public async Task HttpWait ( HttpWait httpWait = null )
{
if ( _networkStream ! = null ) throw new InvalidOperationException ( "Can't use HttpWait over TCP connection" ) ;
var container = new MsgContainer { messages = [ ] } ;
if ( httpWait ! = null & & NewMsgId ( false ) is var ( hwId , hwSeqno ) )
container . messages . Add ( new ( hwId , hwSeqno , httpWait ) ) ;
if ( CheckMsgsToAck ( ) is MsgsAck msgsAck & & NewMsgId ( false ) is var ( ackId , ackSeqno ) )
container . messages . Add ( new ( ackId , ackSeqno , msgsAck ) ) ;
await SendAsync ( container , false ) ;
}
2022-04-11 12:08:17 +02:00
internal async Task < T > InvokeBare < T > ( IMethod < T > request )
2021-09-23 13:13:36 +02:00
{
2023-04-02 13:44:23 +02:00
if ( _bareRpc ! = null ) throw new WTException ( "A bare request is already undergoing" ) ;
2022-10-08 15:06:36 +02:00
retry :
2024-11-18 13:44:32 +01:00
var bareRpc = _bareRpc = new Rpc { type = typeof ( T ) } ;
2022-04-06 18:38:54 +02:00
await SendAsync ( request , false , _bareRpc ) ;
2024-11-18 13:44:32 +01:00
var result = await bareRpc . Task ;
2022-10-08 15:06:36 +02:00
if ( result is ReactorError ) goto retry ;
return ( T ) result ;
2021-09-23 13:13:36 +02:00
}
2022-04-06 18:38:54 +02:00
/// <summary>Call the given TL method <i>(You shouldn't need to use this method directly)</i></summary>
2022-04-11 12:08:17 +02:00
/// <typeparam name="T">Expected type of the returned object</typeparam>
2022-04-06 18:38:54 +02:00
/// <param name="query">TL method structure</param>
/// <returns>Wait for the reply and return the resulting object, or throws an RpcException if an error was replied</returns>
2022-04-11 12:08:17 +02:00
public async Task < T > Invoke < T > ( IMethod < T > query )
2021-09-23 13:13:36 +02:00
{
2025-04-06 19:48:43 +02:00
if ( _dcSession . withoutUpdates & & query is not IMethod < Pong > and not IMethod < FutureSalts > )
2022-07-12 01:31:18 +02:00
query = new TL . Methods . InvokeWithoutUpdates < T > { query = query } ;
2022-04-11 12:08:17 +02:00
bool got503 = false ;
2022-04-06 18:38:54 +02:00
retry :
2022-04-11 12:08:17 +02:00
var rpc = new Rpc { type = typeof ( T ) } ;
2022-04-06 18:38:54 +02:00
await SendAsync ( query , true , rpc ) ;
2024-11-18 13:44:32 +01:00
while ( _httpClient ! = null & & ! rpc . Task . IsCompleted )
await HttpWait ( _httpWait ) ; // need to wait a bit more in some case
2024-09-30 02:15:10 +02:00
2022-04-06 18:38:54 +02:00
var result = await rpc . Task ;
switch ( result )
2021-09-25 19:43:39 +02:00
{
2022-04-18 22:07:04 +02:00
case null : return default ;
2022-04-11 12:08:17 +02:00
case T resultT : return resultT ;
2022-04-13 15:53:06 +02:00
case RpcError { error_code : var code , error_message : var message } :
int x = - 1 ;
for ( int index = message . Length - 1 ; index > 0 & & ( index = message . LastIndexOf ( '_' , index - 1 ) ) > = 0 ; )
if ( message [ index + 1 ] is > = '0' and < = '9' )
{
int end = + + index ;
do end + + ; while ( end < message . Length & & message [ end ] is > = '0' and < = '9' ) ;
x = int . Parse ( message [ index . . end ] ) ;
message = $"{message[0..index]}X{message[end..]}" ;
break ;
}
if ( code = = 303 & & message . EndsWith ( "_MIGRATE_X" ) )
2021-10-06 07:54:20 +02:00
{
2022-04-13 15:53:06 +02:00
if ( message ! = "FILE_MIGRATE_X" )
2021-10-06 07:54:20 +02:00
{
2024-04-27 12:34:32 +02:00
await MigrateToDC ( x ) ;
2022-04-06 18:38:54 +02:00
goto retry ;
2021-10-06 07:54:20 +02:00
}
2022-04-06 18:38:54 +02:00
}
2022-04-26 21:13:23 +02:00
else if ( code = = 420 & & ( message . EndsWith ( "_WAIT_X" ) | | message . EndsWith ( "_DELAY_X" ) ) )
2022-04-06 18:38:54 +02:00
{
2022-04-11 12:08:17 +02:00
if ( x < = FloodRetryThreshold )
2021-10-06 07:54:20 +02:00
{
2025-04-06 19:48:43 +02:00
if ( x = = 0 ) x = 1 ;
2022-04-11 12:08:17 +02:00
await Task . Delay ( x * 1000 ) ;
2022-04-06 18:38:54 +02:00
goto retry ;
2021-10-06 07:54:20 +02:00
}
}
2022-04-13 15:53:06 +02:00
else if ( code = = - 503 & & ! got503 )
2022-02-27 22:06:13 +01:00
{
2022-04-06 18:38:54 +02:00
got503 = true ;
goto retry ;
2022-02-27 22:06:13 +01:00
}
2025-03-12 02:17:36 +01:00
else if ( code = = 400 & & message = = "CONNECTION_NOT_INITED" )
{
await InitConnection ( ) ;
2025-04-06 19:48:43 +02:00
lock ( _session ) _session . Save ( ) ;
2025-03-12 02:17:36 +01:00
goto retry ;
}
2022-04-13 15:53:06 +02:00
else if ( code = = 500 & & message = = "AUTH_RESTART" )
2025-04-06 19:48:43 +02:00
lock ( _session )
{
_session . UserId = 0 ; // force a full login authorization flow, next time
User = null ;
_session . Save ( ) ;
}
2022-04-13 15:53:06 +02:00
throw new RpcException ( code , message , x ) ;
2022-04-06 18:38:54 +02:00
case ReactorError :
goto retry ;
2021-12-31 08:38:41 +01:00
default :
2023-04-02 13:44:23 +02:00
throw new WTException ( $"{query.GetType().Name} call got a result of type {result.GetType().Name} instead of {typeof(T).Name}" ) ;
2021-12-31 08:38:41 +01:00
}
}
2024-04-03 21:05:07 +02:00
2024-04-27 12:34:32 +02:00
private async Task MigrateToDC ( int dcId )
{
// this is a hack to migrate _dcSession in-place (staying in same Client):
Session . DCSession dcSession ;
lock ( _session )
dcSession = GetOrCreateDCSession ( dcId , _dcSession . DataCenter . flags ) ;
2024-09-07 18:43:52 +02:00
await ResetAsync ( false , false ) ;
2024-04-27 12:34:32 +02:00
_session . MainDC = dcId ;
_dcSession . Client = null ;
_dcSession = dcSession ;
_dcSession . Client = this ;
await ConnectAsync ( ) ;
}
2024-12-04 20:10:05 +01:00
[EditorBrowsable(EditorBrowsableState.Never)]
2024-04-03 21:05:07 +02:00
public async Task < T > InvokeAffected < T > ( IMethod < T > query , long peerId ) where T : Messages_AffectedMessages
{
var result = await Invoke ( query ) ;
2024-09-08 19:16:23 +02:00
if ( OnOwnUpdates ! = null )
RaiseOwnUpdates ( new UpdateShort
{
update = new UpdateAffectedMessages { mbox_id = peerId , pts = result . pts , pts_count = result . pts_count } ,
date = MsgIdToStamp ( _lastRecvMsgId )
} ) ;
2024-04-03 21:05:07 +02:00
return result ;
}
2021-08-04 00:40:09 +02:00
}
}