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 ;
2022-01-17 15:06:29 +01:00
using System.IO.Compression ;
2021-08-04 00:40:09 +02:00
using System.Linq ;
using System.Net ;
using System.Net.Sockets ;
using System.Reflection ;
using System.Security.Cryptography ;
using System.Text ;
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 ;
2021-09-17 03:44:52 +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
{
2021-12-10 14:19:28 +01:00
public class Client : IDisposable
2021-08-04 00:40:09 +02:00
{
2021-10-13 00:27:40 +02:00
/// <summary>This event will be called when an unsollicited update/message is sent by Telegram servers</summary>
/// <remarks>See <see href="https://github.com/wiz0u/WTelegramClient/tree/master/Examples/Program_ListenUpdate.cs">Examples/Program_ListenUpdate.cs</see> for how to use this</remarks>
2021-11-07 16:52:58 +01:00
public event Action < IObject > Update ;
2022-01-03 18:15:32 +01:00
public delegate Task < TcpClient > TcpFactory ( string host , int port ) ;
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 ;
/// <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 ;
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 ;
2021-12-07 15:26:53 +01:00
/// <summary>Number of seconds between each keep-alive ping. Increase this if you have a slow connection</summary>
public int PingInterval { get ; set ; } = 60 ;
2021-11-06 05:22:33 +01:00
/// <summary>Is this Client instance the main or a secondary DC session</summary>
2021-09-28 16:12:20 +02:00
public bool IsMainDC = > ( _dcSession ? . DataCenter ? . id ? ? 0 ) = = _session . MainDC ;
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-01-03 18:15:32 +01:00
2021-11-12 20:50:39 +01:00
/// <summary>Used to indicate progression of file download/upload</summary>
/// <param name="totalSize">total size of file in bytes, or 0 if unknown</param>
public delegate void ProgressCallback ( long transmitted , long totalSize ) ;
2021-09-05 01:08:16 +02:00
2021-08-04 00:40:09 +02:00
private readonly Func < string , string > _config ;
private readonly int _apiId ;
private readonly string _apiHash ;
private readonly Session _session ;
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 ;
2021-11-07 16:52:58 +01:00
private IObject _lastSentMsg ;
2021-08-14 15:15:41 +02:00
private long _lastRecvMsgId ;
2021-08-04 00:40:09 +02:00
private readonly List < long > _msgsToAck = new ( ) ;
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 ;
2021-08-14 15:15:41 +02:00
private long _bareRequest ;
2021-08-13 00:28:34 +02:00
private readonly Dictionary < long , ( Type type , TaskCompletionSource < object > tcs ) > _pendingRequests = new ( ) ;
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-10-06 07:54:20 +02:00
private const int FilePartSize = 512 * 1024 ;
2021-11-21 00:43:39 +01:00
private const string ConnectionShutDown = "Could not read payload length : Connection shut down" ;
2021-10-06 07:54:20 +02:00
private readonly SemaphoreSlim _parallelTransfers = new ( 10 ) ; // max parallel part uploads/downloads
2021-09-29 04:38:39 +02:00
#if MTPROTO1
private readonly SHA1 _sha1 = SHA1 . Create ( ) ;
private readonly SHA1 _sha1Recv = SHA1 . Create ( ) ;
#else
private readonly SHA256 _sha256 = SHA256 . Create ( ) ;
private readonly SHA256 _sha256Recv = SHA256 . Create ( ) ;
#endif
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
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>
2021-09-03 00:06:48 +02:00
public Client ( Func < string , string > configProvider = null )
2021-08-04 00:40:09 +02:00
{
_config = configProvider ? ? DefaultConfigOrAsk ;
2021-08-04 12:03:43 +02:00
_apiId = int . Parse ( Config ( "api_id" ) ) ;
_apiHash = Config ( "api_hash" ) ;
2021-08-06 07:28:54 +02:00
_session = Session . LoadOrCreate ( Config ( "session_pathname" ) , Convert . FromHexString ( _apiHash ) ) ;
2021-09-28 16:12:20 +02:00
if ( _session . MainDC ! = 0 ) _session . DCSessions . TryGetValue ( _session . MainDC , out _dcSession ) ;
_dcSession ? ? = new ( ) { Id = Helpers . RandomLong ( ) } ;
_dcSession . Client = this ;
2021-11-04 03:10:03 +01:00
var version = Assembly . GetExecutingAssembly ( ) . GetCustomAttribute < AssemblyInformationalVersionAttribute > ( ) . InformationalVersion ;
Helpers . Log ( 1 , $"WTelegramClient {version[..version.IndexOf('+')]} 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 ;
_apiId = cloneOf . _apiId ;
_apiHash = cloneOf . _apiHash ;
_session = cloneOf . _session ;
2022-01-13 14:22:52 +01:00
TcpHandler = cloneOf . TcpHandler ;
MTProxyUrl = cloneOf . MTProxyUrl ;
PingInterval = cloneOf . PingInterval ;
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 )
= > _config ( what ) ? ? DefaultConfig ( what ) ? ? throw new ApplicationException ( "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
"server_address" = > "149.154.167.40:443" ,
#else
"server_address" = > "149.154.167.50:443" ,
#endif
"device_model" = > Environment . Is64BitOperatingSystem ? "PC 64bit" : "PC 32bit" ,
"system_version" = > System . Runtime . InteropServices . RuntimeInformation . OSDescription ,
2021-12-28 12:12:38 +01:00
"app_version" = > ( Assembly . GetEntryAssembly ( ) ? ? AppDomain . CurrentDomain . GetAssemblies ( ) . FirstOrDefault ( a = > a . EntryPoint ! = null ) ) ? . GetName ( ) . Version . ToString ( ) ? ? "0.0" ,
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" ,
2021-12-05 11:47:52 +01:00
"verification_code" or "password" = > AskConfig ( what ) ,
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
{
2021-10-16 02:01:49 +02:00
if ( config = = "api_id" ) Console . WriteLine ( "Welcome! You can obtain your api_id/api_hash at https://my.telegram.org/apps" ) ;
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>
/// <param name="accountPassword">Password validation configuration. You can obtain this though an Update event as part of the login process</param>
/// <param name="password">The password to validate</param>
public static Task < InputCheckPasswordSRP > InputCheckPassword ( Account_Password accountPassword , string password )
= > Check2FA ( accountPassword , ( ) = > Task . FromResult ( password ) ) ;
2021-08-13 00:28:34 +02:00
public void Dispose ( )
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 2 , $"{_dcSession.DcID}>Disposing the client" ) ;
2021-09-29 02:51:48 +02:00
Reset ( false , IsMainDC ) ;
2021-12-10 14:19:28 +01:00
GC . SuppressFinalize ( this ) ;
2021-08-13 00:28:34 +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>
2021-09-29 02:51:48 +02:00
public void Reset ( bool resetUser = true , bool resetSessions = true )
2021-08-06 07:28:54 +02:00
{
2021-10-01 02:22:26 +02:00
try
{
if ( CheckMsgsToAck ( ) is MsgsAck msgsAck )
2021-11-07 16:50:59 +01:00
SendAsync ( msgsAck , false ) . Wait ( 1000 ) ;
2021-10-01 02:22:26 +02:00
}
catch ( Exception )
{
}
2021-08-13 00:28:34 +02:00
_cts ? . Cancel ( ) ;
2021-09-16 04:47:15 +02:00
_sendSemaphore = new ( 0 ) ;
2021-08-13 00:28:34 +02:00
_reactorTask = null ;
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 ;
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 )
{
altSession . Client . Dispose ( ) ;
altSession . Client = null ;
}
2021-09-23 09:27:52 +02:00
}
2021-09-29 02:51:48 +02:00
if ( resetUser )
2021-11-07 09:09:15 +01:00
_session . UserId = 0 ;
2021-08-06 07:28:54 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Establish connection to Telegram servers</summary>
/// <remarks>Config callback is queried for: <b>server_address</b></remarks>
/// <returns>Most methods of this class are async (Task), so please use <see langword="await"/></returns>
2021-09-28 16:12:20 +02:00
public async Task ConnectAsync ( )
{
lock ( this )
_connecting ? ? = DoConnectAsync ( ) ;
await _connecting ;
}
2021-10-25 02:40:15 +02:00
static async Task < TcpClient > DefaultTcpHandler ( string host , int port )
{
var tcpClient = new TcpClient ( ) ;
try
{
await tcpClient . ConnectAsync ( host , port ) ;
}
catch ( Exception )
{
tcpClient . Dispose ( ) ;
throw ;
}
return tcpClient ;
}
2021-09-28 16:12:20 +02:00
private async Task DoConnectAsync ( )
2021-08-04 00:40:09 +02:00
{
2022-01-11 04:14:23 +01:00
_cts = new ( ) ;
2022-01-03 18:15:32 +01:00
IPEndPoint endpoint = null ;
byte [ ] preamble , secret = null ;
int dcId = _dcSession ? . DcID ? ? 0 ;
if ( dcId = = 0 ) dcId = 2 ;
if ( MTProxyUrl ! = null )
2021-10-01 02:22:26 +02:00
{
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
if ( ! IsMainDC ) dcId = - dcId ;
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
2022-01-11 04:14:23 +01:00
var secretBytes = secret = str . All ( "0123456789ABCDEFabcdef" . Contains ) ? Convert . FromHexString ( str ) :
2022-01-03 18:15:32 +01:00
System . Convert . FromBase64String ( str . Replace ( '_' , '/' ) . Replace ( '-' , '+' ) + new string ( '=' , ( 2147483644 - str . Length ) % 4 ) ) ;
2022-01-11 04:14:23 +01:00
var tlsMode = secret . Length > = 21 & & secret [ 0 ] = = 0xEE ;
if ( tlsMode | | ( secret . Length = = 17 & & secret [ 0 ] = = 0xDD ) )
2021-10-01 02:22:26 +02:00
{
2022-01-03 18:15:32 +01:00
_paddedMode = true ;
secret = secret [ 1. . 17 ] ;
2021-10-06 07:54:20 +02:00
}
2022-01-03 18:15:32 +01:00
else if ( secret . Length ! = 16 ) throw new ArgumentException ( "Invalid/unsupported secret" , nameof ( secret ) ) ;
Helpers . Log ( 2 , $"Connecting to DC {dcId} via MTProxy {server}:{port}..." ) ;
_tcpClient = await TcpHandler ( server , port ) ;
2022-01-11 04:14:23 +01:00
_networkStream = _tcpClient . GetStream ( ) ;
if ( tlsMode )
_networkStream = await TlsStream . HandshakeAsync ( _networkStream , secret , secretBytes [ 17. . ] , _cts . Token ) ;
2022-01-03 18:15:32 +01:00
#else
throw new Exception ( "Library was not compiled with OBFUSCATION symbol" ) ;
#endif
}
else
{
endpoint = _dcSession ? . EndPoint ? ? Compat . IPEndPoint_Parse ( Config ( "server_address" ) ) ;
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
TcpClient tcpClient = null ;
try
2021-10-06 07:54:20 +02:00
{
2022-01-03 18:15:32 +01:00
try
{
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
}
catch ( SocketException ex ) // cannot connect to target endpoint, try to find an alternate
2021-10-01 02:22:26 +02:00
{
2022-01-03 18:15:32 +01:00
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 )
2021-10-01 02:22:26 +02:00
{
2022-01-03 18:15:32 +01:00
var altOptions = _session . DcOptions . Where ( dco = > dco . id = = _dcSession . DataCenter . id & & dco . flags ! = _dcSession . DataCenter . flags
& & ( dco . flags & ( DcOption . Flags . cdn | DcOption . Flags . tcpo_only | DcOption . Flags . media_only ) ) = = 0 )
. OrderBy ( dco = > dco . flags ) ;
// try alternate addresses for this DC
foreach ( var dcOption in altOptions )
2021-10-06 07:54:20 +02:00
{
2022-01-03 18:15:32 +01:00
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 ) ;
_dcSession . DataCenter = dcOption ;
break ;
}
catch ( SocketException ) { }
2021-10-06 07:54:20 +02:00
}
2021-10-01 02:22:26 +02:00
}
2022-01-03 18:15:32 +01:00
if ( tcpClient = = null )
{
endpoint = Compat . IPEndPoint_Parse ( Config ( "server_address" ) ) ; // re-ask callback for an address
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 ) ) ;
_dcSession ? ? = new ( ) { Id = Helpers . RandomLong ( ) } ;
_dcSession . Client = this ;
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
tcpClient = await TcpHandler ( endpoint . Address . ToString ( ) , endpoint . Port ) ;
}
2021-10-01 02:22:26 +02:00
}
}
2022-01-03 18:15:32 +01:00
catch ( Exception )
{
tcpClient ? . Dispose ( ) ;
throw ;
}
_tcpClient = tcpClient ;
2022-01-11 04:14:23 +01:00
_networkStream = _tcpClient . GetStream ( ) ;
2021-10-01 02:22:26 +02:00
}
2022-01-03 18:15:32 +01:00
byte protocolId = ( byte ) ( _paddedMode ? 0xDD : 0xEE ) ;
#if OBFUSCATION
( _sendCtr , _recvCtr , preamble ) = InitObfuscation ( secret , protocolId , dcId ) ;
#else
preamble = new byte [ ] { protocolId , protocolId , protocolId , protocolId } ;
#endif
2022-01-11 04:14:23 +01:00
await _networkStream . WriteAsync ( preamble , 0 , preamble . Length , _cts . Token ) ;
2022-01-03 18:15:32 +01:00
2021-09-24 13:21:35 +02:00
_saltChangeCounter = 0 ;
2021-09-17 03:12:23 +02:00
_reactorTask = Reactor ( _networkStream , _cts ) ;
2021-09-16 04:47:15 +02:00
_sendSemaphore . Release ( ) ;
2021-08-04 00:40:09 +02:00
2021-09-23 09:27:52 +02:00
try
{
2021-09-28 16:12:20 +02:00
if ( _dcSession . AuthKeyID = = 0 )
await CreateAuthorizationKey ( this , _dcSession ) ;
2021-09-23 09:27:52 +02:00
var keepAliveTask = KeepAlive ( _cts . Token ) ;
2021-11-07 16:50:59 +01:00
TLConfig = await this . InvokeWithLayer ( Layer . Version ,
2021-11-10 17:26:40 +01:00
new TL . Methods . InitConnection < Config >
2021-11-10 02:17:08 +01:00
{
api_id = _apiId ,
device_model = Config ( "device_model" ) ,
system_version = Config ( "system_version" ) ,
app_version = Config ( "app_version" ) ,
system_lang_code = Config ( "system_lang_code" ) ,
lang_pack = Config ( "lang_pack" ) ,
lang_code = Config ( "lang_code" ) ,
2021-11-10 17:26:40 +01:00
query = new TL . Methods . Help_GetConfig ( )
2021-11-10 02:17:08 +01:00
} ) ;
2021-10-01 02:22:26 +02:00
_session . DcOptions = TLConfig . dc_options ;
2021-09-24 13:21:35 +02:00
_saltChangeCounter = 0 ;
2021-09-28 16:12:20 +02:00
if ( _dcSession . DataCenter = = null )
2021-09-23 09:27:52 +02:00
{
2021-10-01 02:22:26 +02:00
_dcSession . DataCenter = _session . DcOptions . Where ( dc = > dc . id = = TLConfig . this_dc )
2022-01-03 18:15:32 +01:00
. OrderByDescending ( dc = > dc . ip_address = = endpoint ? . Address . ToString ( ) )
. ThenByDescending ( dc = > dc . port = = endpoint ? . Port ) . First ( ) ;
2021-09-28 16:12:20 +02:00
_session . DCSessions [ TLConfig . this_dc ] = _dcSession ;
2021-09-23 09:27:52 +02:00
}
if ( _session . MainDC = = 0 ) _session . MainDC = TLConfig . this_dc ;
}
finally
{
_session . Save ( ) ;
}
2021-09-16 04:47:15 +02:00
Helpers . Log ( 2 , $"Connected to {(TLConfig.test_mode ? " Test DC " : " DC ")} {TLConfig.this_dc}... {TLConfig.flags & (Config.Flags)~0xE00}" ) ;
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>
/// <param name="dcId">ID of the Data Center</param>
/// <param name="media_only">Session will be used only for transferring media</param>
/// <param name="connect">Connect immediately</param>
/// <returns></returns>
2021-10-01 02:22:26 +02:00
public async Task < Client > GetClientForDC ( int dcId , bool media_only = true , 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 )
{
2021-10-01 02:22:26 +02:00
altSession = GetOrCreateDCSession ( dcId , _dcSession . DataCenter . flags | ( media_only ? DcOption . Flags . media_only : 0 ) ) ;
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 ;
2021-11-07 09:09:15 +01:00
if ( _session . UserId ! = 0 & & IsMainDC & & altSession . UserId ! = _session . UserId )
2021-09-28 16:12:20 +02:00
exported = await this . Auth_ExportAuthorization ( dcId ) ;
await altSession . Client . ConnectAsync ( ) ;
if ( exported ! = null )
{
var authorization = await altSession . Client . Auth_ImportAuthorization ( exported . id , exported . bytes ) ;
if ( authorization is not Auth_Authorization { user : User user } )
throw new ApplicationException ( "Failed to get Authorization: " + authorization . GetType ( ) . Name ) ;
altSession . UserId = user . id ;
}
}
finally
{
_semaphore . Release ( ) ;
}
}
return altSession . Client ;
}
2021-10-01 02:22:26 +02:00
private Session . DCSession GetOrCreateDCSession ( int dcId , DcOption . Flags flags )
2021-09-28 16:12:20 +02:00
{
if ( _session . DCSessions . TryGetValue ( dcId , out var dcSession ) )
2021-10-01 02:22:26 +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
// try to find the most appropriate DcOption for this DC
2021-12-05 07:21:30 +01:00
if ( ( dcSession ? . AuthKeyID ? ? 0 ) = = 0 ) // we will need to negociate an AuthKey => can't use media_only DC
flags & = ~ DcOption . Flags . media_only ;
2021-10-01 02:22:26 +02:00
var dcOptions = _session . DcOptions . Where ( dc = > dc . id = = dcId ) . OrderBy ( dc = > dc . flags ^ flags ) ;
2021-09-28 16:12:20 +02:00
var dcOption = dcOptions . FirstOrDefault ( ) ? ? throw new ApplicationException ( $"Could not find adequate dc_option for DC {dcId}" ) ;
2021-10-01 02:22:26 +02:00
dcSession ? ? = new Session . DCSession { Id = Helpers . RandomLong ( ) } ; // create new session only if not already existing
dcSession . DataCenter = dcOption ;
return _session . DCSessions [ dcId ] = dcSession ;
2021-09-28 16:12:20 +02:00
}
internal DateTime MsgIdToStamp ( long serverMsgId )
= > new ( ( serverMsgId > > 32 ) * 10000000 - _dcSession . ServerTicksOffset + 621355968000000000L , DateTimeKind . Utc ) ;
internal ( long msgId , int seqno ) NewMsgId ( bool isContent )
{
int seqno ;
long msgId = DateTime . UtcNow . Ticks + _dcSession . ServerTicksOffset - 621355968000000000L ;
msgId = msgId * 428 + ( msgId > > 24 ) * 25110956 ; // approximately unixtime*2^32 and divisible by 4
lock ( _session )
{
if ( msgId < = _dcSession . LastSentMsgId ) msgId = _dcSession . LastSentMsgId + = 4 ; else _dcSession . LastSentMsgId = msgId ;
seqno = isContent ? _dcSession . Seqno + + * 2 + 1 : _dcSession . Seqno * 2 ;
_session . Save ( ) ;
2021-08-06 07:28:54 +02:00
}
2021-09-28 16:12:20 +02:00
return ( msgId , seqno ) ;
2021-08-04 00:40:09 +02:00
}
2021-09-17 03:12:23 +02:00
private async Task KeepAlive ( CancellationToken ct )
2021-08-04 00:40:09 +02:00
{
2021-09-17 03:12:23 +02:00
int ping_id = _random . Next ( ) ;
2021-09-16 04:47:15 +02:00
while ( ! ct . IsCancellationRequested )
2021-08-04 00:40:09 +02:00
{
2021-12-07 15:26:53 +01:00
await Task . Delay ( PingInterval * 1000 , ct ) ;
2021-09-24 13:21:35 +02:00
if ( _saltChangeCounter > 0 ) - - _saltChangeCounter ;
2021-09-27 03:25:28 +02:00
#if DEBUG
2021-12-07 15:26:53 +01:00
await this . PingDelayDisconnect ( ping_id + + , PingInterval * 5 ) ;
2021-09-27 03:25:28 +02:00
#else
2021-12-07 15:26:53 +01:00
await this . PingDelayDisconnect ( ping_id + + , PingInterval * 5 / 4 ) ;
2021-09-27 03:25:28 +02:00
#endif
2021-09-17 03:12:23 +02:00
}
}
2022-01-11 04:14:23 +01:00
private async Task Reactor ( Stream stream , CancellationTokenSource cts )
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 ] ;
2021-09-17 03:12:23 +02:00
while ( ! cts . IsCancellationRequested )
{
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
{
2022-01-11 04:14:23 +01:00
if ( await stream . FullReadAsync ( data , 4 , cts . Token ) ! = 4 )
2021-11-21 00:43:39 +01:00
throw new ApplicationException ( 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 )
throw new ApplicationException ( "Could not read frame data : Invalid payload length" ) ;
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 ) ] ;
2022-01-11 04:14:23 +01:00
if ( await stream . FullReadAsync ( data , payloadLen , cts . Token ) ! = payloadLen )
2021-09-23 05:37:00 +02:00
throw new ApplicationException ( "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
{
2021-09-17 03:12:23 +02:00
if ( cts . IsCancellationRequested ) return ;
2021-11-21 00:43:39 +01:00
Helpers . Log ( 5 , $"{_dcSession.DcID}>An exception occured in the reactor: {ex}" ) ;
2021-09-16 04:47:15 +02:00
var oldSemaphore = _sendSemaphore ;
2021-09-17 03:12:23 +02:00
await oldSemaphore . WaitAsync ( cts . Token ) ; // 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 ( ) ;
Reset ( false , false ) ;
2021-09-28 16:12:20 +02:00
_reactorReconnects = ( _reactorReconnects + 1 ) % MaxAutoReconnects ;
2021-11-21 00:43:39 +01:00
if ( ! IsMainDC & & _pendingRequests . Count < = 1 & & ex is ApplicationException { Message : ConnectionShutDown } or IOException { InnerException : SocketException } )
if ( _pendingRequests . Values . FirstOrDefault ( ) is var ( type , tcs ) & & ( type is null | | type = = typeof ( Pong ) ) )
_reactorReconnects = 0 ;
2021-09-28 16:12:20 +02:00
if ( _reactorReconnects ! = 0 )
{
await Task . Delay ( 5000 ) ;
await ConnectAsync ( ) ; // start a new reactor after 5 secs
lock ( _pendingRequests ) // retry all pending requests
{
foreach ( var ( _ , tcs ) in _pendingRequests . Values )
tcs . SetResult ( reactorError ) ;
_pendingRequests . Clear ( ) ;
_bareRequest = 0 ;
}
2021-11-07 09:09:15 +01:00
// TODO: implement an Updates gaps handling system? https://core.telegram.org/api/updates
2021-11-12 20:50:39 +01:00
if ( IsMainDC )
{
var udpatesState = await this . Updates_GetState ( ) ; // this call reenables incoming Updates
OnUpdate ( udpatesState ) ;
}
2021-09-28 16:12:20 +02:00
}
else
throw ;
}
catch
2021-09-16 04:47:15 +02:00
{
lock ( _pendingRequests ) // abort all pending requests
{
foreach ( var ( _ , tcs ) in _pendingRequests . Values )
2021-09-27 17:07:56 +02:00
tcs . SetException ( ex ) ;
2021-09-16 04:47:15 +02:00
_pendingRequests . Clear ( ) ;
_bareRequest = 0 ;
}
2021-09-28 16:12:20 +02:00
OnUpdate ( reactorError ) ;
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
}
}
2021-11-07 16:52:58 +01:00
internal IObject ReadFrame ( byte [ ] data , int dataLen )
2021-08-04 00:40:09 +02:00
{
2021-09-23 05:37:00 +02:00
if ( dataLen = = 4 & & 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
throw new ApplicationException ( $"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 ) ;
2021-09-28 16:12:20 +02:00
if ( authKeyId ! = _dcSession . AuthKeyID )
2021-08-20 02:13:58 +02:00
throw new ApplicationException ( $"Received a packet encrypted with unexpected key {authKeyId:X}" ) ;
2021-08-04 00:40:09 +02:00
if ( authKeyId = = 0 ) // Unencrypted message
{
2021-09-23 05:37:00 +02:00
using var reader = new TL . BinaryReader ( new MemoryStream ( data , 8 , dataLen - 8 ) , this ) ;
2021-08-14 15:15:41 +02:00
long msgId = _lastRecvMsgId = reader . ReadInt64 ( ) ;
2021-08-04 00:40:09 +02:00
if ( ( msgId & 1 ) = = 0 ) throw new ApplicationException ( $"Invalid server msgId {msgId}" ) ;
int length = reader . ReadInt32 ( ) ;
2022-01-03 18:15:32 +01:00
dataLen - = 20 ;
if ( length > dataLen | | length < dataLen - ( _paddedMode ? 15 : 0 ) )
throw new ApplicationException ( $"Unexpected unencrypted length {length} != {dataLen}" ) ;
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 ")}" ) ;
2021-08-12 12:37:56 +02:00
return obj ;
2021-08-04 00:40:09 +02:00
}
else
{
2021-09-29 04:38:39 +02:00
#if MTPROTO1
byte [ ] decrypted_data = EncryptDecryptMessage ( data . AsSpan ( 24 , dataLen - 24 ) , false , _dcSession . AuthKey , data , 8 , _sha1Recv ) ;
#else
2022-01-03 18:15:32 +01:00
byte [ ] decrypted_data = EncryptDecryptMessage ( data . AsSpan ( 24 , ( dataLen - 24 ) & ~ 0xF ) , false , _dcSession . AuthKey , data , 8 , _sha256Recv ) ;
2021-09-29 04:38:39 +02:00
#endif
2021-08-04 00:40:09 +02:00
if ( decrypted_data . Length < 36 ) // header below+ctorNb
throw new ApplicationException ( $"Decrypted packet too small: {decrypted_data.Length}" ) ;
2021-08-30 01:31:08 +02:00
using var reader = new TL . BinaryReader ( new MemoryStream ( decrypted_data ) , this ) ;
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
if ( _lastRecvMsgId = = 0 ) // resync ServerTicksOffset on first message
_dcSession . ServerTicksOffset = ( msgId > > 32 ) * 10000000 - DateTime . UtcNow . Ticks + 621355968000000000L ;
var msgStamp = MsgIdToStamp ( _lastRecvMsgId = msgId ) ;
2021-08-04 00:40:09 +02:00
2021-09-28 16:12:20 +02:00
if ( serverSalt ! = _dcSession . Salt ) // salt change happens every 30 min
2021-08-05 16:29:58 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 2 , $"{_dcSession.DcID}>Server salt has changed: {_dcSession.Salt:X} -> {serverSalt:X}" ) ;
_dcSession . Salt = serverSalt ;
2021-09-24 13:21:35 +02:00
_saltChangeCounter + = 20 ; // counter is decreased by KeepAlive every minute (we have margin of 10)
if ( _saltChangeCounter > = 30 )
throw new ApplicationException ( $"Server salt changed too often! Security issue?" ) ;
2021-08-05 16:29:58 +02:00
}
2021-09-28 16:12:20 +02:00
if ( sessionId ! = _dcSession . Id ) throw new ApplicationException ( $"Unexpected session ID {sessionId} != {_dcSession.Id}" ) ;
2021-08-04 00:40:09 +02:00
if ( ( msgId & 1 ) = = 0 ) throw new ApplicationException ( $"Invalid server msgId {msgId}" ) ;
2021-09-16 04:47:15 +02:00
if ( ( seqno & 1 ) ! = 0 ) lock ( _msgsToAck ) _msgsToAck . Add ( msgId ) ;
2021-08-10 14:40:41 +02:00
#if MTPROTO1
2021-08-04 00:40:09 +02:00
if ( decrypted_data . Length - 32 - length is < 0 or > 15 ) throw new ApplicationException ( $"Unexpected decrypted message_data_length {length} / {decrypted_data.Length - 32}" ) ;
2021-09-29 04:38:39 +02:00
if ( ! data . AsSpan ( 8 , 16 ) . SequenceEqual ( _sha1Recv . ComputeHash ( decrypted_data , 0 , 32 + length ) . AsSpan ( 4 ) ) )
2021-08-04 00:40:09 +02:00
throw new ApplicationException ( $"Mismatch between MsgKey & decrypted SHA1" ) ;
2021-08-10 14:40:41 +02:00
#else
if ( decrypted_data . Length - 32 - length is < 12 or > 1024 ) throw new ApplicationException ( $"Unexpected decrypted message_data_length {length} / {decrypted_data.Length - 32}" ) ;
2021-09-29 04:38:39 +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 ) ) )
2022-01-03 18:15:32 +01:00
throw new ApplicationException ( $"Mismatch between MsgKey & decrypted SHA256" ) ;
2021-10-17 23:35:14 +02:00
_sha256Recv . Initialize ( ) ;
2021-08-10 14:40:41 +02:00
#endif
2021-08-13 07:06:44 +02:00
var ctorNb = reader . ReadUInt32 ( ) ;
2021-12-30 23:45:28 +01:00
if ( ctorNb ! = Layer . BadMsgCtor & & ( msgStamp - DateTime . UtcNow ) . Ticks / TimeSpan . TicksPerSecond is > 30 or < - 300 )
{ // msg_id values that belong over 30 seconds in the future or over 300 seconds in the past are to be ignored.
Helpers . Log ( 1 , $"{_dcSession.DcID}>Ignoring 0x{ctorNb:X8} because of wrong timestamp {msgStamp:u} (svc)" ) ;
return null ;
}
2021-09-17 04:53:02 +02:00
if ( ctorNb = = Layer . MsgContainerCtor )
2021-08-13 07:06:44 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {" MsgContainer ",-40} {msgStamp:u} (svc)" ) ;
2021-08-13 07:06:44 +02:00
return ReadMsgContainer ( reader ) ;
}
2021-09-17 04:53:02 +02:00
else if ( ctorNb = = Layer . RpcResultCtor )
2021-08-13 07:06:44 +02:00
{
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {" RpcResult ",-40} {msgStamp:u}" ) ;
2021-08-13 07:06:44 +02:00
return ReadRpcResult ( reader ) ;
}
else
{
var obj = reader . ReadTLObject ( ctorNb ) ;
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Receiving {obj.GetType().Name,-40} {msgStamp:u} {((seqno & 1) != 0 ? "" : " ( svc ) ")} {((msgId & 2) == 0 ? " " : " NAR ")}" ) ;
2021-08-13 07:06:44 +02:00
return obj ;
}
2021-08-12 12:37:56 +02:00
}
2021-08-04 00:40:09 +02:00
static string TransportError ( int error_code ) = > error_code switch
{
404 = > "Auth key not found" ,
429 = > "Transport flood" ,
2021-11-03 18:20:54 +01:00
_ = > Enum . GetName ( typeof ( HttpStatusCode ) , error_code ) ? ? "Transport error"
2021-08-04 00:40:09 +02:00
} ;
}
2021-11-07 16:52:58 +01:00
private async Task < long > SendAsync ( IObject msg , bool isContent )
2021-09-16 04:47:15 +02:00
{
2021-09-28 16:12:20 +02:00
if ( _dcSession . AuthKeyID ! = 0 & & isContent & & CheckMsgsToAck ( ) is MsgsAck msgsAck )
2021-09-16 04:47:15 +02:00
{
2021-09-28 16:12:20 +02:00
var ackMsg = NewMsgId ( false ) ;
var mainMsg = NewMsgId ( true ) ;
2021-11-07 16:50:59 +01:00
await SendAsync ( MakeContainer ( ( msgsAck , ackMsg ) , ( msg , mainMsg ) ) , false ) ;
2021-09-16 04:47:15 +02:00
return mainMsg . msgId ;
}
2021-09-28 16:12:20 +02:00
( long msgId , int seqno ) = NewMsgId ( isContent & & _dcSession . AuthKeyID ! = 0 ) ;
2021-09-16 04:47:15 +02:00
await _sendSemaphore . WaitAsync ( ) ;
try
{
using var memStream = new MemoryStream ( 1024 ) ;
using var writer = new BinaryWriter ( memStream , Encoding . UTF8 ) ;
2021-09-17 03:44:52 +02:00
writer . Write ( 0 ) ; // int32 payload_len (to be patched with payload length)
2021-09-16 04:47:15 +02:00
2021-09-28 16:12:20 +02:00
if ( _dcSession . AuthKeyID = = 0 ) // send unencrypted message
2021-09-16 04:47:15 +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)
2021-11-09 01:43:27 +01:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_')}..." ) ;
2021-11-21 00:43:39 +01:00
writer . WriteTLObject ( msg ) ; // bytes message_data
2021-09-17 15:09:12 +02:00
BinaryPrimitives . WriteInt32LittleEndian ( memStream . GetBuffer ( ) . AsSpan ( 20 ) , ( int ) memStream . Length - 24 ) ; // patch message_data_length
2021-09-16 04:47:15 +02:00
}
else
{
using var clearStream = new MemoryStream ( 1024 ) ;
using var clearWriter = new BinaryWriter ( clearStream , Encoding . UTF8 ) ;
2021-09-27 03:25:28 +02:00
#if MTPROTO1
2021-09-16 04:47:15 +02:00
const int prepend = 0 ;
2021-09-27 03:25:28 +02:00
#else
2021-09-16 04:47:15 +02:00
const int prepend = 32 ;
2021-09-28 16:12:20 +02:00
clearWriter . Write ( _dcSession . AuthKey , 88 , prepend ) ;
2021-09-27 03:25:28 +02:00
#endif
2021-09-28 16:12:20 +02:00
clearWriter . Write ( _dcSession . Salt ) ; // int64 salt
clearWriter . Write ( _dcSession . Id ) ; // int64 session_id
2021-09-16 04:47:15 +02:00
clearWriter . Write ( msgId ) ; // int64 message_id
clearWriter . Write ( seqno ) ; // int32 msg_seqno
clearWriter . Write ( 0 ) ; // int32 message_data_length (to be patched)
if ( ( seqno & 1 ) ! = 0 )
2021-11-09 01:43:27 +01:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} #{(short)msgId.GetHashCode():X4}" ) ;
2021-09-16 04:47:15 +02:00
else
2021-11-09 01:43:27 +01:00
Helpers . Log ( 1 , $"{_dcSession.DcID}>Sending {msg.GetType().Name.TrimEnd('_'),-40} {MsgIdToStamp(msgId):u} (svc)" ) ;
2021-11-21 00:43:39 +01:00
clearWriter . WriteTLObject ( msg ) ; // bytes message_data
2021-09-16 04:47:15 +02:00
int clearLength = ( int ) clearStream . Length - prepend ; // length before padding (= 32 + message_data_length)
int padding = ( 0x7FFFFFF0 - clearLength ) % 16 ;
2021-09-27 03:25:28 +02:00
#if ! MTPROTO1
2021-09-16 04:47:15 +02:00
padding + = _random . Next ( 1 , 64 ) * 16 ; // MTProto 2.0 padding must be between 12..1024 with total length divisible by 16
2021-09-27 03:25:28 +02:00
#endif
2021-09-16 04:47:15 +02:00
clearStream . SetLength ( prepend + clearLength + padding ) ;
byte [ ] clearBuffer = clearStream . GetBuffer ( ) ;
BinaryPrimitives . WriteInt32LittleEndian ( clearBuffer . AsSpan ( prepend + 28 ) , clearLength - 32 ) ; // patch message_data_length
RNG . GetBytes ( clearBuffer , prepend + clearLength , padding ) ;
2021-09-27 03:25:28 +02:00
#if MTPROTO1
2021-09-29 04:38:39 +02:00
var msgKeyLarge = _sha1 . ComputeHash ( clearBuffer , 0 , clearLength ) ; // padding excluded from computation!
2021-09-16 04:47:15 +02:00
const int msgKeyOffset = 4 ; // msg_key = low 128-bits of SHA1(plaintext)
2021-09-29 04:38:39 +02:00
byte [ ] encrypted_data = EncryptDecryptMessage ( clearBuffer . AsSpan ( prepend , clearLength + padding ) , true , _dcSession . AuthKey , msgKeyLarge , msgKeyOffset , _sha1 ) ;
2021-09-27 03:25:28 +02:00
#else
2021-09-29 04:38:39 +02:00
var msgKeyLarge = _sha256 . ComputeHash ( clearBuffer , 0 , prepend + clearLength + padding ) ;
2021-09-16 04:47:15 +02:00
const int msgKeyOffset = 8 ; // msg_key = middle 128-bits of SHA256(authkey_part+plaintext+padding)
2021-09-29 04:38:39 +02:00
byte [ ] encrypted_data = EncryptDecryptMessage ( clearBuffer . AsSpan ( prepend , clearLength + padding ) , true , _dcSession . AuthKey , msgKeyLarge , msgKeyOffset , _sha256 ) ;
2021-09-27 03:25:28 +02:00
#endif
2021-09-16 04:47:15 +02:00
2021-09-28 16:12:20 +02:00
writer . Write ( _dcSession . AuthKeyID ) ; // int64 auth_key_id
2021-09-16 04:47:15 +02:00
writer . Write ( msgKeyLarge , msgKeyOffset , 16 ) ; // int128 msg_key
writer . Write ( encrypted_data ) ; // bytes encrypted_data
}
2022-01-03 18:15:32 +01:00
if ( _paddedMode ) // Padded intermediate mode => append random padding
{
var padding = new byte [ _random . Next ( 16 ) ] ;
RNG . GetBytes ( padding ) ;
writer . Write ( padding ) ;
}
2021-09-16 04:47:15 +02:00
var buffer = memStream . GetBuffer ( ) ;
int frameLength = ( int ) memStream . Length ;
2021-09-17 03:44:52 +02:00
BinaryPrimitives . WriteInt32LittleEndian ( buffer , frameLength - 4 ) ; // patch payload_len with correct value
2022-01-03 18:15:32 +01:00
#if OBFUSCATION
_sendCtr . EncryptDecrypt ( buffer , frameLength ) ;
#endif
await _networkStream . WriteAsync ( buffer , 0 , frameLength ) ;
2021-11-07 16:50:59 +01:00
_lastSentMsg = msg ;
2021-09-16 04:47:15 +02:00
}
finally
{
_sendSemaphore . Release ( ) ;
}
return msgId ;
}
2021-08-30 01:31:08 +02:00
internal MsgContainer ReadMsgContainer ( TL . BinaryReader reader )
2021-08-04 00:40:09 +02:00
{
2021-08-13 07:06:44 +02:00
int count = reader . ReadInt32 ( ) ;
var array = new _Message [ count ] ;
for ( int i = 0 ; i < count ; i + + )
2021-08-04 00:40:09 +02:00
{
2021-08-13 07:06:44 +02:00
var msg = array [ i ] = new _Message
{
msg_id = reader . ReadInt64 ( ) ,
seqno = reader . ReadInt32 ( ) ,
bytes = reader . ReadInt32 ( ) ,
} ;
2022-01-11 04:14:23 +01:00
if ( ( msg . seqno & 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
{
2021-09-28 16:12:20 +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 ) ;
2021-09-28 16:12:20 +02:00
Helpers . Log ( 1 , $" → {obj.GetType().Name,-38} {MsgIdToStamp(msg.msg_id):u} {((msg.seqno & 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 ( ) ) ;
}
reader . BaseStream . Position = pos + array [ i ] . bytes ;
2021-08-05 16:29:58 +02:00
}
2021-08-13 07:06:44 +02:00
return new MsgContainer { messages = array } ;
2021-08-04 00:40:09 +02:00
}
2021-08-30 01:31:08 +02:00
private RpcResult ReadRpcResult ( TL . BinaryReader reader )
2021-08-04 00:40:09 +02:00
{
2021-08-13 07:06:44 +02:00
long msgId = reader . ReadInt64 ( ) ;
2021-08-14 15:15:41 +02:00
var ( type , tcs ) = PullPendingRequest ( msgId ) ;
2021-08-14 08:55:30 +02:00
object result ;
2021-08-14 15:15:41 +02:00
if ( tcs ! = null )
2021-08-13 00:28:34 +02:00
{
2021-12-07 16:35:23 +01:00
try
{
if ( ! type . IsArray )
result = reader . ReadTLValue ( type ) ;
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 )
using ( var gzipReader = new TL . BinaryReader ( new GZipStream ( new MemoryStream ( reader . ReadTLBytes ( ) ) , CompressionMode . Decompress ) , reader . Client ) )
result = gzipReader . ReadTLValue ( type ) ;
else
{
reader . BaseStream . Position - = 4 ;
result = reader . ReadTLValue ( type ) ;
}
2021-12-07 16:35:23 +01:00
}
if ( type . IsEnum ) result = Enum . ToObject ( 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
Helpers . Log ( 1 , $" → {result?.GetType().Name,-37} #{(short)msgId.GetHashCode():X4}" ) ;
2021-12-07 16:35:23 +01:00
tcs . SetResult ( result ) ;
}
catch ( Exception ex )
2021-10-01 02:22:26 +02:00
{
2021-12-07 16:35:23 +01:00
tcs . SetException ( ex ) ;
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
string typeName ;
var ctorNb = reader . ReadUInt32 ( ) ;
if ( ctorNb = = Layer . VectorCtor )
{
reader . BaseStream . Position - = 4 ;
var array = reader . ReadTLVector ( typeof ( IObject [ ] ) ) ;
if ( array . Length > 0 )
{
for ( type = array . GetValue ( 0 ) . GetType ( ) ; type . BaseType ! = typeof ( object ) ; ) type = type . BaseType ;
typeName = type . Name + "[]" ;
}
else
typeName = "object[]" ;
result = array ;
}
else
{
result = reader . ReadTLObject ( ctorNb ) ;
typeName = result ? . GetType ( ) . Name ;
}
2021-09-28 16:12:20 +02:00
if ( MsgIdToStamp ( msgId ) > = _session . SessionStart )
2022-01-11 04:14:23 +01:00
Helpers . Log ( 4 , $" → {typeName,-37} for unknown msgId #{(short)msgId.GetHashCode():X4}" ) ;
2021-08-13 00:28:34 +02:00
else
2022-01-11 04:14:23 +01: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
}
2021-08-14 15:15:41 +02:00
private ( Type type , TaskCompletionSource < object > tcs ) PullPendingRequest ( long msgId )
{
( Type type , TaskCompletionSource < object > tcs ) request ;
lock ( _pendingRequests )
if ( _pendingRequests . TryGetValue ( msgId , out request ) )
_pendingRequests . Remove ( msgId ) ;
return request ;
}
2021-12-16 14:51:47 +01:00
internal async Task < X > InvokeBare < X > ( IMethod < X > request )
2021-08-04 00:40:09 +02:00
{
2021-09-16 04:47:15 +02:00
if ( _bareRequest ! = 0 ) throw new ApplicationException ( "A bare request is already undergoing" ) ;
2021-08-14 15:15:41 +02:00
var msgId = await SendAsync ( request , false ) ;
2021-09-27 17:07:56 +02:00
var tcs = new TaskCompletionSource < object > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
2021-08-14 15:15:41 +02:00
lock ( _pendingRequests )
_pendingRequests [ msgId ] = ( typeof ( X ) , tcs ) ;
_bareRequest = msgId ;
return ( X ) await tcs . Task ;
2021-08-04 00:40:09 +02:00
}
2021-12-16 14:51:47 +01:00
/// <summary>Call the given TL method <i>(You shouldn't need to use this method directly)</i></summary>
2021-11-06 05:22:33 +01:00
/// <typeparam name="X">Expected type of the returned object</typeparam>
2021-12-16 14:51:47 +01:00
/// <param name="query">TL method structure</param>
2021-11-06 05:22:33 +01:00
/// <returns>Wait for the reply and return the resulting object, or throws an RpcException if an error was replied</returns>
2021-12-16 14:51:47 +01:00
public async Task < X > Invoke < X > ( IMethod < X > query )
2021-08-04 00:40:09 +02:00
{
2021-08-13 00:28:34 +02:00
retry :
2021-12-16 14:51:47 +01:00
var msgId = await SendAsync ( query , true ) ;
2021-09-27 17:07:56 +02:00
var tcs = new TaskCompletionSource < object > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
2021-08-14 15:15:41 +02:00
lock ( _pendingRequests )
_pendingRequests [ msgId ] = ( typeof ( X ) , tcs ) ;
var result = await tcs . Task ;
2021-08-13 00:28:34 +02:00
switch ( result )
{
case X resultX : return resultX ;
case RpcError rpcError :
2021-08-14 08:55:30 +02:00
int number ;
if ( rpcError . error_code = = 303 & & ( ( number = rpcError . error_message . IndexOf ( "_MIGRATE_" ) ) > 0 ) )
{
2021-09-28 16:12:20 +02:00
if ( ! rpcError . error_message . StartsWith ( "FILE_" ) )
{
number = int . Parse ( rpcError . error_message [ ( number + 9 ) . . ] ) ;
// this is a hack to migrate _dcSession in-place (staying in same Client):
Session . DCSession dcSession ;
lock ( _session )
2021-10-01 02:22:26 +02:00
dcSession = GetOrCreateDCSession ( number , _dcSession . DataCenter . flags ) ;
2021-09-29 02:51:48 +02:00
Reset ( false , false ) ;
2021-09-28 16:12:20 +02:00
_session . MainDC = number ;
_dcSession . Client = null ;
_dcSession = dcSession ;
_dcSession . Client = this ;
await ConnectAsync ( ) ;
goto retry ;
}
2021-08-14 08:55:30 +02:00
}
else if ( rpcError . error_code = = 420 & & ( ( number = rpcError . error_message . IndexOf ( "_WAIT_" ) ) > 0 ) )
2021-08-04 00:40:09 +02:00
{
2021-08-14 08:55:30 +02:00
number = int . Parse ( rpcError . error_message [ ( number + 6 ) . . ] ) ;
2021-11-12 20:50:39 +01:00
if ( number < = FloodRetryThreshold )
2021-08-20 03:41:00 +02:00
{
await Task . Delay ( number * 1000 ) ;
goto retry ;
}
2021-08-04 00:40:09 +02:00
}
2021-08-27 14:14:24 +02:00
else if ( rpcError . error_code = = 500 & & rpcError . error_message = = "AUTH_RESTART" )
{
2021-11-07 09:09:15 +01:00
_session . UserId = 0 ; // force a full login authorization flow, next time
2021-08-27 14:14:24 +02:00
_session . Save ( ) ;
}
2021-08-20 03:41:00 +02:00
throw new RpcException ( rpcError . error_code , rpcError . error_message ) ;
2021-09-28 16:12:20 +02:00
case ReactorError :
goto retry ;
2021-08-13 00:28:34 +02:00
default :
2021-12-16 14:51:47 +01:00
throw new ApplicationException ( $"{query.GetType().Name} call got a result of type {result.GetType().Name} instead of {typeof(X).Name}" ) ;
2021-08-04 00:40:09 +02:00
}
}
2021-08-13 00:28:34 +02:00
private MsgsAck CheckMsgsToAck ( )
2021-08-04 00:40:09 +02:00
{
lock ( _msgsToAck )
2021-08-13 00:28:34 +02:00
{
if ( _msgsToAck . Count = = 0 ) return null ;
var msgsAck = new MsgsAck { msg_ids = _msgsToAck . ToArray ( ) } ;
_msgsToAck . Clear ( ) ;
return msgsAck ;
}
}
2021-11-07 16:52:58 +01:00
private static MsgContainer MakeContainer ( params ( IObject obj , ( long msgId , int seqno ) ) [ ] msgs )
2021-11-07 16:50:59 +01:00
= > new ( )
2021-08-13 00:28:34 +02:00
{
2021-11-07 16:50:59 +01:00
messages = msgs . Select ( msg = > new _Message
2021-08-04 00:40:09 +02:00
{
2021-11-07 16:50:59 +01:00
msg_id = msg . Item2 . msgId ,
seqno = msg . Item2 . seqno ,
body = msg . obj
} ) . ToArray ( )
2021-08-13 00:28:34 +02:00
} ;
2021-11-07 16:52:58 +01:00
private async Task HandleMessageAsync ( IObject obj )
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 ) ;
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 ( ) ;
bool retryLast = badMsgNotification . bad_msg_id = = _dcSession . LastSentMsgId ;
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
{
2021-12-30 23:45:28 +01:00
case 16 :
case 17 :
_dcSession . LastSentMsgId = 0 ;
var localTime = DateTime . UtcNow ;
_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" ) ;
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)
if ( _dcSession . Seqno < = 1 )
retryLast = false ;
else
{
Reset ( false , false ) ;
_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)
_dcSession . Salt = ( ( BadServerSalt ) badMsgNotification ) . new_server_salt ;
break ;
default :
retryLast = false ;
break ;
}
if ( retryLast )
{
var newMsgId = await SendAsync ( lastSentMsg , true ) ;
lock ( _pendingRequests )
if ( _pendingRequests . TryGetValue ( badMsgNotification . bad_msg_id , out var t ) )
{
_pendingRequests . Remove ( badMsgNotification . bad_msg_id ) ;
_pendingRequests [ newMsgId ] = t ;
}
2021-08-27 22:44:43 +02:00
}
2021-12-04 00:14:15 +01:00
else if ( PullPendingRequest ( badMsgNotification . bad_msg_id ) . tcs is TaskCompletionSource < object > tcs )
{
if ( _bareRequest = = badMsgNotification . bad_msg_id ) _bareRequest = 0 ;
tcs . SetException ( new ApplicationException ( $"BadMsgNotification {badMsgNotification.error_code}" ) ) ;
}
else
OnUpdate ( obj ) ;
2021-08-27 22:44:43 +02:00
break ;
2021-08-04 00:40:09 +02:00
default :
2021-08-14 15:15:41 +02:00
if ( _bareRequest ! = 0 )
2021-08-13 00:28:34 +02:00
{
2021-08-14 15:15:41 +02:00
var ( type , tcs ) = PullPendingRequest ( _bareRequest ) ;
2021-08-27 22:44:43 +02:00
if ( type ? . IsAssignableFrom ( obj . GetType ( ) ) = = true )
2021-08-14 15:15:41 +02:00
{
_bareRequest = 0 ;
2021-09-27 17:07:56 +02:00
tcs . SetResult ( obj ) ;
2021-09-05 01:08:16 +02:00
break ;
2021-08-14 15:15:41 +02:00
}
2021-08-13 00:28:34 +02:00
}
2021-09-05 01:08:16 +02:00
OnUpdate ( 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
{
2021-09-27 17:07:56 +02:00
var tcs = PullPendingRequest ( msgId ) . tcs ;
2021-08-14 15:15:41 +02:00
if ( tcs ! = null )
2021-09-27 17:07:56 +02:00
tcs . SetResult ( result ) ;
2021-09-03 00:06:48 +02:00
else
2021-09-05 01:08:16 +02:00
OnUpdate ( obj ) ;
}
}
2021-11-07 16:52:58 +01:00
private void OnUpdate ( IObject obj )
2021-09-05 01:08:16 +02:00
{
try
{
Update ? . Invoke ( obj ) ;
}
catch ( Exception ex )
{
Helpers . Log ( 4 , $"Update callback on {obj.GetType().Name} raised {ex}" ) ;
2021-08-14 15:15:41 +02:00
}
2021-08-04 00:40:09 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Login as a bot (if not already logged-in).</summary>
/// <remarks>Config callback is queried for: <b>bot_token</b>
/// <br/>Bots can only call API methods marked with [bots: ✓] in their documentation. </remarks>
/// <returns>Detail about the logged-in bot</returns>
2021-08-26 17:46:45 +02:00
public async Task < User > LoginBotIfNeeded ( )
2021-08-24 17:24:46 +02:00
{
2021-10-15 04:24:34 +02:00
await ConnectAsync ( ) ;
2021-08-27 14:14:24 +02:00
string botToken = Config ( "bot_token" ) ;
2021-11-07 09:09:15 +01:00
if ( _session . UserId ! = 0 ) // a user is already logged-in
2021-11-06 05:22:33 +01:00
{
2021-08-25 15:32:25 +02:00
try
{
2021-11-07 09:09:15 +01:00
var users = await this . Users_GetUsers ( new [ ] { InputUser . Self } ) ; // this calls also reenable incoming Updates
var self = users [ 0 ] as User ;
if ( self . id = = long . Parse ( botToken . Split ( ':' ) [ 0 ] ) )
2021-10-28 22:48:43 +02:00
{
2021-11-07 09:09:15 +01:00
_session . UserId = _dcSession . UserId = self . id ;
return self ;
2021-10-28 22:48:43 +02:00
}
2021-11-07 09:09:15 +01:00
Helpers . Log ( 3 , $"Current logged user {self.id} mismatched bot_token. Logging out and in..." ) ;
2021-08-25 15:32:25 +02:00
}
catch ( Exception ex )
{
2021-10-28 22:48:43 +02:00
Helpers . Log ( 4 , $"Error while verifying current bot! ({ex.Message}) Proceeding to login..." ) ;
2021-08-25 15:32:25 +02:00
}
2021-09-17 04:53:02 +02:00
await this . Auth_LogOut ( ) ;
2021-11-07 09:09:15 +01:00
_session . UserId = _dcSession . UserId = 0 ;
2021-08-27 14:14:24 +02:00
}
2021-09-17 04:53:02 +02:00
var authorization = await this . Auth_ImportBotAuthorization ( 0 , _apiId , _apiHash , botToken ) ;
2021-12-05 07:21:30 +01:00
return LoginAlreadyDone ( authorization ) ;
2021-08-24 17:24:46 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Login as a user (if not already logged-in).
/// <br/><i>(this method calls ConnectAsync if necessary)</i></summary>
/// <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>user_id</b> (alt validation)</remarks>
/// <param name="settings">(optional) Preference for verification_code sending</param>
/// <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>
2021-08-26 17:46:45 +02:00
public async Task < User > LoginUserIfNeeded ( CodeSettings settings = null )
2021-08-04 00:40:09 +02:00
{
2021-10-15 04:24:34 +02:00
await ConnectAsync ( ) ;
2021-08-27 22:44:43 +02:00
string phone_number = null ;
2021-11-07 09:09:15 +01:00
if ( _session . UserId ! = 0 ) // a user is already logged-in
2021-08-27 14:14:24 +02:00
{
2021-08-25 15:32:25 +02:00
try
{
2021-11-07 09:09:15 +01:00
var users = await this . Users_GetUsers ( new [ ] { InputUser . Self } ) ; // this calls also reenable incoming Updates
var self = users [ 0 ] as User ;
// check user_id or phone_number match currently logged-in user
2021-12-10 14:19:28 +01:00
if ( ( long . TryParse ( _config ( "user_id" ) , out long id ) & & ( id = = - 1 | | self . id = = id ) ) | |
2021-11-07 09:09:15 +01:00
self . phone = = string . Concat ( ( phone_number = Config ( "phone_number" ) ) . Where ( char . IsDigit ) ) )
2021-09-17 03:25:27 +02:00
{
2021-11-07 09:09:15 +01:00
_session . UserId = _dcSession . UserId = self . id ;
return self ;
2021-09-17 03:25:27 +02:00
}
2021-11-07 09:09:15 +01:00
Helpers . Log ( 3 , $"Current logged user {self.id} mismatched user_id or phone_number. Logging out and in..." ) ;
2021-08-25 15:32:25 +02:00
}
catch ( Exception ex )
{
2021-10-28 22:48:43 +02:00
Helpers . Log ( 4 , $"Error while verifying current user! ({ex.Message}) Proceeding to login..." ) ;
2021-08-25 15:32:25 +02:00
}
2021-09-17 04:53:02 +02:00
await this . Auth_LogOut ( ) ;
2021-11-07 09:09:15 +01:00
_session . UserId = _dcSession . UserId = 0 ;
2021-08-27 14:14:24 +02:00
}
2021-08-27 22:44:43 +02:00
phone_number ? ? = Config ( "phone_number" ) ;
2021-09-29 02:51:48 +02:00
Auth_SentCode sentCode ;
2021-08-06 07:28:54 +02:00
try
2021-08-04 00:40:09 +02:00
{
2021-09-29 02:51:48 +02:00
sentCode = await this . Auth_SendCode ( phone_number , _apiId , _apiHash , settings ? ? = new ( ) ) ;
2021-08-06 07:28:54 +02:00
}
2021-09-29 02:51:48 +02:00
catch ( RpcException ex ) when ( ex . Code = = 500 & & ex . Message = = "AUTH_RESTART" )
2021-08-06 07:28:54 +02:00
{
2021-09-29 02:51:48 +02:00
sentCode = await this . Auth_SendCode ( phone_number , _apiId , _apiHash , settings ? ? = new ( ) ) ;
2021-08-06 07:28:54 +02:00
}
2021-12-13 15:28:06 +01:00
resent :
var timeout = DateTime . UtcNow + TimeSpan . FromSeconds ( sentCode . timeout ) ;
OnUpdate ( sentCode ) ;
2021-09-29 02:51:48 +02:00
Helpers . Log ( 3 , $"A verification code has been sent via {sentCode.type.GetType().Name[17..]}" ) ;
2021-12-05 07:21:30 +01:00
Auth_AuthorizationBase authorization = null ;
for ( int retry = 1 ; authorization = = null ; retry + + )
2021-09-29 02:51:48 +02:00
try
{
2021-12-05 11:47:52 +01:00
var verification_code = await ConfigAsync ( "verification_code" ) ;
2021-12-13 15:28:06 +01:00
if ( verification_code = = "" & & sentCode . next_type ! = 0 )
{
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 ;
}
sentCode = await this . Auth_ResendCode ( phone_number , sentCode . phone_code_hash ) ;
goto resent ;
}
2021-09-29 02:51:48 +02:00
authorization = await this . Auth_SignIn ( phone_number , sentCode . phone_code_hash , verification_code ) ;
}
catch ( RpcException e ) when ( e . Code = = 401 & & e . Message = = "SESSION_PASSWORD_NEEDED" )
{
var accountPassword = await this . Account_GetPassword ( ) ;
2021-12-14 13:39:43 +01:00
OnUpdate ( accountPassword ) ;
2021-12-05 11:47:52 +01:00
var checkPasswordSRP = await Check2FA ( accountPassword , ( ) = > ConfigAsync ( "password" ) ) ;
2021-09-29 02:51:48 +02:00
authorization = await this . Auth_CheckPassword ( checkPasswordSRP ) ;
}
catch ( RpcException e ) when ( e . Code = = 400 & & e . Message = = "PHONE_CODE_INVALID" & & retry ! = 3 )
{
}
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 ) ;
2021-09-03 00:06:48 +02:00
if ( signUpRequired . terms_of_service ! = null )
2021-09-05 01:08:16 +02:00
OnUpdate ( signUpRequired . terms_of_service ) ; // 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
2021-09-17 04:53:02 +02:00
authorization = await this . Auth_SignUp ( phone_number , sentCode . phone_code_hash , first_name , last_name ) ;
2021-08-06 07:28:54 +02:00
}
2021-12-05 07:21:30 +01:00
return LoginAlreadyDone ( authorization ) ;
}
/// <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 )
{
2021-08-06 07:28:54 +02:00
if ( authorization is not Auth_Authorization { user : User user } )
2021-08-04 00:40:09 +02:00
throw new ApplicationException ( "Failed to get Authorization: " + authorization . GetType ( ) . Name ) ;
2021-11-07 09:09:15 +01:00
_session . UserId = _dcSession . UserId = user . id ;
2021-08-06 07:28:54 +02:00
_session . Save ( ) ;
2021-08-04 00:40:09 +02:00
return user ;
}
2021-08-05 16:29:58 +02:00
2021-12-30 17:37:25 +01:00
/// <summary>Enable the collection of id/access_hash pairs (experimental)</summary>
public bool CollectAccessHash { get ; set ; }
readonly Dictionary < Type , Dictionary < long , long > > _accessHashes = new ( ) ;
public IEnumerable < KeyValuePair < long , long > > AllAccessHashesFor < T > ( ) where T : IObject
= > _accessHashes . GetValueOrDefault ( typeof ( T ) ) ;
/// <summary>Retrieve the access_hash associated with this id (for a TL class) if it was collected</summary>
/// <remarks>This requires <see cref="CollectAccessHash"/> to be set to <see langword="true"/> first.
/// <br/>See <see href="https://github.com/wiz0u/WTelegramClient/tree/master/Examples/Program_CollectAccessHash.cs">Examples/Program_CollectAccessHash.cs</see> for how to use this</remarks>
/// <typeparam name="T">a TL object class. For example User, Channel or Photo</typeparam>
public long GetAccessHashFor < T > ( long id ) where T : IObject
{
lock ( _accessHashes )
return _accessHashes . GetOrCreate ( typeof ( T ) ) . TryGetValue ( id , out var access_hash ) ? access_hash : 0 ;
}
public void SetAccessHashFor < T > ( long id , long access_hash ) where T : IObject
{
lock ( _accessHashes )
_accessHashes . GetOrCreate ( typeof ( T ) ) [ id ] = access_hash ;
}
internal void UpdateAccessHash ( object obj , Type type , object access_hash )
{
if ( ! CollectAccessHash ) return ;
if ( access_hash is not long accessHash ) return ;
if ( type . GetField ( "id" ) is not FieldInfo idField ) return ;
if ( idField . GetValue ( obj ) is not long id )
if ( idField . GetValue ( obj ) is not int idInt ) return ;
else id = idInt ;
lock ( _accessHashes )
_accessHashes . GetOrCreate ( type ) [ id ] = accessHash ;
}
#region TL - Helpers
2021-08-05 16:29:58 +02:00
/// <summary>Helper function to upload a file to Telegram</summary>
2021-11-06 05:22:33 +01:00
/// <param name="pathname">Path to the file to upload</param>
2021-11-12 20:50:39 +01:00
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
2021-08-05 16:29:58 +02:00
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
2021-11-12 20:50:39 +01:00
public Task < InputFileBase > UploadFileAsync ( string pathname , ProgressCallback progress = null )
= > UploadFileAsync ( File . OpenRead ( pathname ) , Path . GetFileName ( pathname ) , progress ) ;
2021-08-05 16:29:58 +02:00
2021-11-06 05:22:33 +01:00
/// <summary>Helper function to upload a file to Telegram</summary>
/// <param name="stream">Content of the file to upload</param>
/// <param name="filename">Name of the file</param>
2021-11-12 20:50:39 +01:00
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
2021-11-06 05:22:33 +01:00
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
2021-11-12 20:50:39 +01:00
public async Task < InputFileBase > UploadFileAsync ( Stream stream , string filename , ProgressCallback progress = null )
2021-08-05 16:29:58 +02:00
{
using var md5 = MD5 . Create ( ) ;
using ( stream )
{
2021-11-12 20:50:39 +01:00
long transmitted = 0 , length = stream . Length ;
2021-08-05 16:29:58 +02:00
var isBig = length > = 10 * 1024 * 1024 ;
2021-10-06 07:54:20 +02:00
int file_total_parts = ( int ) ( ( length - 1 ) / FilePartSize ) + 1 ;
2021-08-05 16:29:58 +02:00
long file_id = Helpers . RandomLong ( ) ;
int file_part = 0 , read ;
2021-08-13 07:06:44 +02:00
var tasks = new Dictionary < int , Task > ( ) ;
bool abort = false ;
for ( long bytesLeft = length ; ! abort & & bytesLeft ! = 0 ; file_part + + )
2021-08-05 16:29:58 +02:00
{
2021-10-06 07:54:20 +02:00
var bytes = new byte [ Math . Min ( FilePartSize , bytesLeft ) ] ;
2022-01-11 04:14:23 +01:00
read = await stream . FullReadAsync ( bytes , bytes . Length , default ) ;
2021-10-06 07:54:20 +02:00
await _parallelTransfers . WaitAsync ( ) ;
2021-11-12 20:50:39 +01:00
bytesLeft - = read ;
2021-08-13 07:06:44 +02:00
var task = SavePart ( file_part , bytes ) ;
lock ( tasks ) tasks [ file_part ] = task ;
if ( ! isBig )
2021-08-09 11:41:50 +02:00
md5 . TransformBlock ( bytes , 0 , read , null , 0 ) ;
2021-10-06 07:54:20 +02:00
if ( read < FilePartSize & & bytesLeft ! = 0 ) throw new ApplicationException ( $"Failed to fully read stream ({read},{bytesLeft})" ) ;
2021-08-13 07:06:44 +02:00
async Task SavePart ( int file_part , byte [ ] bytes )
{
try
{
if ( isBig )
2021-09-17 04:53:02 +02:00
await this . Upload_SaveBigFilePart ( file_id , file_part , file_total_parts , bytes ) ;
2021-08-13 07:06:44 +02:00
else
2021-09-17 04:53:02 +02:00
await this . Upload_SaveFilePart ( file_id , file_part , bytes ) ;
2021-11-12 20:50:39 +01:00
lock ( tasks ) { transmitted + = bytes . Length ; tasks . Remove ( file_part ) ; }
progress ? . Invoke ( transmitted , length ) ;
2021-08-13 07:06:44 +02:00
}
catch ( Exception )
{
abort = true ;
2021-10-06 07:54:20 +02:00
throw ;
2021-08-13 07:06:44 +02:00
}
finally
{
2021-10-06 07:54:20 +02:00
_parallelTransfers . Release ( ) ;
2021-08-13 07:06:44 +02:00
}
}
2021-08-05 16:29:58 +02:00
}
2021-10-06 07:54:20 +02:00
Task [ ] remainingTasks ;
lock ( tasks ) remainingTasks = tasks . Values . ToArray ( ) ;
await Task . WhenAll ( remainingTasks ) ; // wait completion and eventually propagate any task exception
2021-08-13 07:06:44 +02:00
if ( ! isBig ) md5 . TransformFinalBlock ( Array . Empty < byte > ( ) , 0 , 0 ) ;
2021-08-05 16:29:58 +02:00
return isBig ? new InputFileBig { id = file_id , parts = file_total_parts , name = filename }
: new InputFile { id = file_id , parts = file_total_parts , name = filename , md5_checksum = md5 . Hash } ;
}
}
2021-12-08 15:05:33 +01:00
/// <summary>Helper function to send a media message more easily</summary>
2021-11-06 05:22:33 +01:00
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></param>
/// <param name="mediaFile">Media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
2021-08-05 16:29:58 +02:00
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, or a MIME type to send as a document</param>
2021-11-06 05:22:33 +01:00
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
2021-11-09 01:43:27 +01:00
/// <returns>The transmitted message confirmed by Telegram</returns>
public Task < Message > SendMediaAsync ( InputPeer peer , string caption , InputFileBase mediaFile , string mimeType = null , int reply_to_msg_id = 0 , MessageEntity [ ] entities = null , DateTime schedule_date = default )
2021-08-05 16:29:58 +02:00
{
var filename = mediaFile is InputFile iFile ? iFile . name : ( mediaFile as InputFileBig ) ? . name ;
2021-12-15 20:55:24 +01:00
mimeType ? ? = Path . GetExtension ( filename ) ? . ToLowerInvariant ( ) switch
2021-08-05 16:29:58 +02:00
{
".jpg" or ".jpeg" or ".png" or ".bmp" = > "photo" ,
".gif" = > "image/gif" ,
".webp" = > "image/webp" ,
".mp4" = > "video/mp4" ,
".mp3" = > "audio/mpeg" ,
".wav" = > "audio/x-wav" ,
_ = > "" , // send as generic document with undefined MIME type
} ;
if ( mimeType = = "photo" )
return SendMessageAsync ( peer , caption , new InputMediaUploadedPhoto { file = mediaFile } ,
2021-11-06 05:22:33 +01:00
reply_to_msg_id , entities , schedule_date ) ;
2021-08-05 16:29:58 +02:00
var attributes = filename = = null ? Array . Empty < DocumentAttribute > ( ) : new [ ] { new DocumentAttributeFilename { file_name = filename } } ;
return SendMessageAsync ( peer , caption , new InputMediaUploadedDocument
{
file = mediaFile , mime_type = mimeType , attributes = attributes
2021-11-06 05:22:33 +01:00
} , reply_to_msg_id , entities , schedule_date ) ;
2021-08-05 16:29:58 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Helper function to send a text or media message easily</summary>
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
/// <param name="text">The plain text of the message (or media caption)</param>
/// <param name="media">An instance of <see cref="InputMedia">InputMedia</see>-derived class, or <see langword="null"/> if there is no associated media</param>
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
/// <param name="entities">Text formatting entities. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
/// <param name="disable_preview">Should website/media preview be shown or not, for URLs in your message</param>
2021-11-09 01:43:27 +01:00
/// <returns>The transmitted message as confirmed by Telegram</returns>
public async Task < Message > SendMessageAsync ( InputPeer peer , string text , InputMedia media = null , int reply_to_msg_id = 0 , MessageEntity [ ] entities = null , DateTime schedule_date = default , bool disable_preview = false )
2021-08-05 16:29:58 +02:00
{
2021-11-09 01:43:27 +01:00
UpdatesBase updates ;
long random_id = Helpers . RandomLong ( ) ;
2021-08-09 11:41:50 +02:00
if ( media = = null )
2021-11-10 01:20:54 +01:00
updates = await this . Messages_SendMessage ( peer , text , random_id , no_webpage : disable_preview , entities : entities ,
reply_to_msg_id : reply_to_msg_id = = 0 ? null : reply_to_msg_id , schedule_date : schedule_date = = default ? null : schedule_date ) ;
2021-08-09 11:41:50 +02:00
else
2021-11-10 01:20:54 +01:00
updates = await this . Messages_SendMedia ( peer , media , text , random_id , entities : entities ,
reply_to_msg_id : reply_to_msg_id = = 0 ? null : reply_to_msg_id , schedule_date : schedule_date = = default ? null : schedule_date ) ;
2021-11-09 01:43:27 +01:00
OnUpdate ( updates ) ;
int msgId = - 1 ;
foreach ( var update in updates . UpdateList )
{
switch ( update )
{
case UpdateMessageID updMsgId when updMsgId . random_id = = random_id : msgId = updMsgId . id ; break ;
case UpdateNewMessage { message : Message message } when message . id = = msgId : return message ;
case UpdateNewScheduledMessage { message : Message schedMsg } when schedMsg . id = = msgId : return schedMsg ;
}
}
if ( updates is UpdateShortSentMessage sent )
{
return new Message
{
flags = ( Message . Flags ) sent . flags | ( reply_to_msg_id = = 0 ? 0 : Message . Flags . has_reply_to ) | ( peer is InputPeerSelf ? 0 : Message . Flags . has_from_id ) ,
id = sent . id , date = sent . date , message = text , entities = sent . entities , media = sent . media , ttl_period = sent . ttl_period ,
reply_to = reply_to_msg_id = = 0 ? null : new MessageReplyHeader { reply_to_msg_id = reply_to_msg_id } ,
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session . UserId } ,
peer_id = InputToPeer ( peer )
} ;
}
return null ;
2021-08-05 16:29:58 +02:00
}
2021-09-23 13:13:36 +02:00
2021-11-09 01:43:27 +01:00
private Peer InputToPeer ( InputPeer peer ) = > peer switch
{
InputPeerSelf = > new PeerUser { user_id = _session . UserId } ,
InputPeerUser ipu = > new PeerUser { user_id = ipu . user_id } ,
InputPeerChat ipc = > new PeerChat { chat_id = ipc . chat_id } ,
InputPeerChannel ipch = > new PeerChannel { channel_id = ipch . channel_id } ,
InputPeerUserFromMessage ipufm = > new PeerUser { user_id = ipufm . user_id } ,
InputPeerChannelFromMessage ipcfm = > new PeerChannel { channel_id = ipcfm . channel_id } ,
_ = > null ,
} ;
2021-11-06 05:22:33 +01:00
/// <summary>Download a photo from Telegram into the outputStream</summary>
/// <param name="photo">The photo to download</param>
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
/// <param name="photoSize">A specific size/version of the photo, or <see langword="null"/> to download the largest version of the photo</param>
2021-11-12 20:50:39 +01:00
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
2021-11-06 05:22:33 +01:00
/// <returns>The file type of the photo</returns>
2021-11-12 20:50:39 +01:00
public async Task < Storage_FileType > DownloadFileAsync ( Photo photo , Stream outputStream , PhotoSizeBase photoSize = null , ProgressCallback progress = null )
2021-09-23 13:13:36 +02:00
{
2021-12-28 12:12:38 +01:00
if ( photoSize is PhotoStrippedSize psp )
return InflateStrippedThumb ( outputStream , psp . bytes ) ? Storage_FileType . jpeg : 0 ;
2021-10-06 07:54:20 +02:00
photoSize ? ? = photo . LargestPhotoSize ;
var fileLocation = photo . ToFileLocation ( photoSize ) ;
2021-11-12 20:50:39 +01:00
return await DownloadFileAsync ( fileLocation , outputStream , photo . dc_id , photoSize . FileSize , progress ) ;
2021-09-23 13:13:36 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Download a document from Telegram into the outputStream</summary>
/// <param name="document">The document to download</param>
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
/// <param name="thumbSize">A specific size/version of the document thumbnail to download, or <see langword="null"/> to download the document itself</param>
2021-11-12 20:50:39 +01:00
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
2021-11-06 05:22:33 +01:00
/// <returns>MIME type of the document/thumbnail</returns>
2021-11-12 20:50:39 +01:00
public async Task < string > DownloadFileAsync ( Document document , Stream outputStream , PhotoSizeBase thumbSize = null , ProgressCallback progress = null )
2021-09-23 13:13:36 +02:00
{
2022-01-11 04:14:23 +01:00
if ( thumbSize is PhotoStrippedSize psp )
2021-12-28 12:12:38 +01:00
return InflateStrippedThumb ( outputStream , psp . bytes ) ? "image/jpeg" : null ;
2021-09-23 13:13:36 +02:00
var fileLocation = document . ToFileLocation ( thumbSize ) ;
2021-11-12 20:50:39 +01:00
var fileType = await DownloadFileAsync ( fileLocation , outputStream , document . dc_id , thumbSize ? . FileSize ? ? document . size , progress ) ;
2021-09-27 03:25:28 +02:00
return thumbSize = = null ? document . mime_type : "image/" + fileType ;
2021-09-23 13:13:36 +02:00
}
2021-11-06 05:22:33 +01:00
/// <summary>Download a file from Telegram into the outputStream</summary>
2021-09-23 13:13:36 +02:00
/// <param name="fileLocation">Telegram file identifier, typically obtained with a .ToFileLocation() call</param>
2021-11-06 05:22:33 +01:00
/// <param name="outputStream">Stream to write file content to. This method does not close/dispose the stream</param>
2021-12-28 06:43:33 +01:00
/// <param name="dc_id">(optional) DC on which the file is stored</param>
2021-11-06 05:22:33 +01:00
/// <param name="fileSize">(optional) Expected file size</param>
2021-11-12 20:50:39 +01:00
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
2021-11-06 05:22:33 +01:00
/// <returns>The file type</returns>
2021-12-28 06:43:33 +01:00
public async Task < Storage_FileType > DownloadFileAsync ( InputFileLocationBase fileLocation , Stream outputStream , int dc_id = 0 , int fileSize = 0 , ProgressCallback progress = null )
2021-09-23 13:13:36 +02:00
{
2021-10-06 07:54:20 +02:00
Storage_FileType fileType = Storage_FileType . unknown ;
2021-12-28 06:43:33 +01:00
var client = dc_id = = 0 ? this : await GetClientForDC ( dc_id , true ) ;
2021-10-06 07:54:20 +02:00
using var writeSem = new SemaphoreSlim ( 1 ) ;
long streamStartPos = outputStream . Position ;
int fileOffset = 0 , maxOffsetSeen = 0 ;
2021-11-12 20:50:39 +01:00
long transmitted = 0 ;
2021-10-06 07:54:20 +02:00
var tasks = new Dictionary < int , Task > ( ) ;
2021-11-13 15:09:28 +01:00
progress ? . Invoke ( 0 , fileSize ) ;
2021-10-06 07:54:20 +02:00
bool abort = false ;
while ( ! abort )
2021-09-25 19:43:39 +02:00
{
2021-10-06 07:54:20 +02:00
await _parallelTransfers . WaitAsync ( ) ;
var task = LoadPart ( fileOffset ) ;
lock ( tasks ) tasks [ fileOffset ] = task ;
2021-12-28 06:43:33 +01:00
if ( dc_id = = 0 ) { await task ; dc_id = client . _dcSession . DcID ; }
2021-10-06 07:54:20 +02:00
fileOffset + = FilePartSize ;
if ( fileSize ! = 0 & & fileOffset > = fileSize )
2021-09-25 19:43:39 +02:00
{
2021-10-06 07:54:20 +02:00
if ( await task ! = ( ( fileSize - 1 ) % FilePartSize ) + 1 )
throw new ApplicationException ( "Downloaded file size does not match expected file size" ) ;
break ;
2021-09-28 16:12:20 +02:00
}
2021-10-06 07:54:20 +02:00
async Task < int > LoadPart ( int offset )
2021-09-28 16:12:20 +02:00
{
2021-10-06 07:54:20 +02:00
Upload_FileBase fileBase ;
try
{
fileBase = await client . Upload_GetFile ( fileLocation , offset , FilePartSize ) ;
}
catch ( RpcException ex ) when ( ex . Code = = 303 & & ex . Message . StartsWith ( "FILE_MIGRATE_" ) )
{
var dcId = int . Parse ( ex . Message [ 13. . ] ) ;
client = await GetClientForDC ( dcId , true ) ;
fileBase = await client . Upload_GetFile ( fileLocation , offset , FilePartSize ) ;
}
catch ( RpcException ex ) when ( ex . Code = = 400 & & ex . Message = = "OFFSET_INVALID" )
{
abort = true ;
return 0 ;
}
catch ( Exception )
{
abort = true ;
throw ;
}
finally
{
_parallelTransfers . Release ( ) ;
}
if ( fileBase is not Upload_File fileData )
throw new ApplicationException ( "Upload_GetFile returned unsupported " + fileBase . GetType ( ) . Name ) ;
if ( fileData . bytes . Length ! = FilePartSize ) abort = true ;
if ( fileData . bytes . Length ! = 0 )
{
fileType = fileData . type ;
await writeSem . WaitAsync ( ) ;
try
{
if ( streamStartPos + offset ! = outputStream . Position ) // if we're about to write out of order
{
await outputStream . FlushAsync ( ) ; // async flush, otherwise Seek would do a sync flush
outputStream . Seek ( streamStartPos + offset , SeekOrigin . Begin ) ;
}
await outputStream . WriteAsync ( fileData . bytes , 0 , fileData . bytes . Length ) ;
maxOffsetSeen = Math . Max ( maxOffsetSeen , offset + fileData . bytes . Length ) ;
2021-11-12 20:50:39 +01:00
transmitted + = fileData . bytes . Length ;
2021-10-06 07:54:20 +02:00
}
catch ( Exception )
{
abort = true ;
throw ;
}
finally
{
writeSem . Release ( ) ;
2021-11-12 20:50:39 +01:00
progress ? . Invoke ( transmitted , fileSize ) ;
2021-10-06 07:54:20 +02:00
}
}
lock ( tasks ) tasks . Remove ( offset ) ;
return fileData . bytes . Length ;
2021-09-28 16:12:20 +02:00
}
2021-10-06 07:54:20 +02:00
}
Task [ ] remainingTasks ;
lock ( tasks ) remainingTasks = tasks . Values . ToArray ( ) ;
await Task . WhenAll ( remainingTasks ) ; // wait completion and eventually propagate any task exception
2021-09-28 16:12:20 +02:00
await outputStream . FlushAsync ( ) ;
2021-10-06 07:54:20 +02:00
outputStream . Seek ( streamStartPos + maxOffsetSeen , SeekOrigin . Begin ) ;
return fileType ;
2021-09-23 13:13:36 +02:00
}
2021-12-07 00:34:57 +01:00
2021-12-28 06:43:33 +01:00
/// <summary>Download the profile photo for a given peer into the outputStream</summary>
/// <param name="peer">User, Chat or Channel</param>
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
/// <param name="big">Whether to download the high-quality version of the picture</param>
2021-12-28 09:38:22 +01:00
/// <param name="miniThumb">Whether to extract the embedded very low-res thumbnail (synchronous, no actual download needed)</param>
2021-12-28 06:43:33 +01:00
/// <returns>The file type of the photo, or 0 if no photo available</returns>
2021-12-28 09:38:22 +01:00
public async Task < Storage_FileType > DownloadProfilePhotoAsync ( IPeerInfo peer , Stream outputStream , bool big = false , bool miniThumb = false )
2021-12-28 06:43:33 +01:00
{
int dc_id ;
2021-12-28 09:38:22 +01:00
long photo_id ;
byte [ ] stripped_thumb ;
2021-12-28 06:43:33 +01:00
switch ( peer )
{
case User user :
if ( user . photo = = null ) return 0 ;
dc_id = user . photo . dc_id ;
2021-12-28 09:38:22 +01:00
photo_id = user . photo . photo_id ;
stripped_thumb = user . photo . stripped_thumb ;
2021-12-28 06:43:33 +01:00
break ;
2021-12-28 09:38:22 +01:00
case ChatBase { Photo : var photo } :
if ( photo = = null ) return 0 ;
dc_id = photo . dc_id ;
photo_id = photo . photo_id ;
stripped_thumb = photo . stripped_thumb ;
2021-12-28 06:43:33 +01:00
break ;
default :
return 0 ;
}
2021-12-28 09:38:22 +01:00
if ( miniThumb & & ! big )
2021-12-28 12:12:38 +01:00
return InflateStrippedThumb ( outputStream , stripped_thumb ) ? Storage_FileType . jpeg : 0 ;
2021-12-28 09:38:22 +01:00
var fileLocation = new InputPeerPhotoFileLocation { peer = peer . ToInputPeer ( ) , photo_id = photo_id } ;
if ( big ) fileLocation . flags | = InputPeerPhotoFileLocation . Flags . big ;
2021-12-28 06:43:33 +01:00
return await DownloadFileAsync ( fileLocation , outputStream , dc_id ) ;
}
2021-12-28 12:12:38 +01:00
private static bool InflateStrippedThumb ( Stream outputStream , byte [ ] stripped_thumb )
{
if ( stripped_thumb = = null | | stripped_thumb . Length < = 3 | | stripped_thumb [ 0 ] ! = 1 )
return false ;
var header = Helpers . StrippedThumbJPG ;
outputStream . Write ( header , 0 , 164 ) ;
outputStream . WriteByte ( stripped_thumb [ 1 ] ) ;
outputStream . WriteByte ( 0 ) ;
outputStream . WriteByte ( stripped_thumb [ 2 ] ) ;
outputStream . Write ( header , 167 , header . Length - 167 ) ;
outputStream . Write ( stripped_thumb , 3 , stripped_thumb . Length - 3 ) ;
outputStream . WriteByte ( 0xff ) ;
outputStream . WriteByte ( 0xd9 ) ;
return true ;
}
2021-12-07 00:34:57 +01:00
/// <summary>Helper method that tries to fetch all participants from a Channel (beyond Telegram server-side limitations)</summary>
/// <param name="channel">The channel to query</param>
2021-12-07 15:07:35 +01:00
/// <param name="includeKickBan">Also fetch the kicked/banned members?</param>
2021-12-07 00:34:57 +01:00
/// <returns>Field count indicates the total count of members. Field participants contains those that were successfully fetched</returns>
/// <remarks>This method can take a few minutes to complete on big channels. It likely won't be able to obtain the full total count of members</remarks>
public async Task < Channels_ChannelParticipants > Channels_GetAllParticipants ( InputChannelBase channel , bool includeKickBan = false )
{
var result = new Channels_ChannelParticipants { chats = new ( ) , users = new ( ) } ;
var sem = new SemaphoreSlim ( 10 ) ; // prevents flooding Telegram with requests
var user_ids = new HashSet < long > ( ) ;
var participants = new List < ChannelParticipantBase > ( ) ;
var tasks = new List < Task >
{
GetWithFilter ( new ChannelParticipantsAdmins ( ) ) ,
GetWithFilter ( new ChannelParticipantsBots ( ) ) ,
GetWithFilter ( new ChannelParticipantsSearch { q = "" } , ( f , c ) = > new ChannelParticipantsSearch { q = f . q + c } ) ,
} ;
var mcf = this . Channels_GetFullChannel ( channel ) ;
tasks . Add ( mcf ) ;
if ( includeKickBan )
{
tasks . Add ( GetWithFilter ( new ChannelParticipantsKicked { q = "" } , ( f , c ) = > new ChannelParticipantsKicked { q = f . q + c } ) ) ;
tasks . Add ( GetWithFilter ( new ChannelParticipantsBanned { q = "" } , ( f , c ) = > new ChannelParticipantsBanned { q = f . q + c } ) ) ;
}
await Task . WhenAll ( tasks ) ;
result . count = ( ( ChannelFull ) mcf . Result . full_chat ) . participants_count ;
result . participants = participants . ToArray ( ) ;
return result ;
async Task GetWithFilter < T > ( T filter , Func < T , char , T > recurse = null ) where T : ChannelParticipantsFilter
{
Channels_ChannelParticipants ccp ;
for ( int offset = 0 ; ; )
{
await sem . WaitAsync ( ) ;
try
{
2021-12-07 15:07:35 +01:00
ccp = await this . Channels_GetParticipants ( channel , filter , offset , 1024 , 0 ) ;
2021-12-07 00:34:57 +01:00
}
finally
{
sem . Release ( ) ;
}
foreach ( var kvp in ccp . chats ) result . chats [ kvp . Key ] = kvp . Value ;
foreach ( var kvp in ccp . users ) result . users [ kvp . Key ] = kvp . Value ;
lock ( participants )
foreach ( var participant in ccp . participants )
if ( user_ids . Add ( participant . UserID ) )
participants . Add ( participant ) ;
offset + = ccp . participants . Length ;
2021-12-07 15:07:35 +01:00
if ( offset > = ccp . count | | ccp . participants . Length = = 0 ) break ;
2021-12-07 00:34:57 +01:00
}
if ( recurse ! = null & & ( ccp . count = = 200 | | ccp . count = = 1000 ) )
await Task . WhenAll ( Enumerable . Range ( 'a' , 26 ) . Select ( c = > GetWithFilter ( recurse ( filter , ( char ) c ) , recurse ) ) ) ;
}
}
2021-08-30 01:31:08 +02:00
2021-12-31 08:38:41 +01:00
public Task < UpdatesBase > AddChatUser ( InputPeer peer , InputUserBase user , int fwd_limit = int . MaxValue ) = > peer switch
{
InputPeerChat chat = > this . Messages_AddChatUser ( chat . chat_id , user , fwd_limit ) ,
InputPeerChannel channel = > this . Channels_InviteToChannel ( channel , new [ ] { user } ) ,
_ = > throw new ArgumentException ( "This method works on Chat & Channel only" ) ,
} ;
public Task < UpdatesBase > DeleteChatUser ( InputPeer peer , InputUser user , bool revoke_history = true ) = > peer switch
{
InputPeerChat chat = > this . Messages_DeleteChatUser ( chat . chat_id , user , revoke_history ) ,
InputPeerChannel channel = > this . Channels_EditBanned ( channel , user , new ChatBannedRights { flags = ChatBannedRights . Flags . view_messages } ) ,
_ = > throw new ArgumentException ( "This method works on Chat & Channel only" ) ,
} ;
public Task < UpdatesBase > LeaveChat ( InputPeer peer , bool revoke_history = true ) = > peer switch
{
InputPeerChat chat = > this . Messages_DeleteChatUser ( chat . chat_id , InputUser . Self , revoke_history ) ,
InputPeerChannel channel = > this . Channels_LeaveChannel ( channel ) ,
_ = > throw new ArgumentException ( "This method works on Chat & Channel only" ) ,
} ;
public async Task < UpdatesBase > EditChatAdmin ( InputPeer peer , InputUserBase user , bool is_admin )
{
switch ( peer )
{
case InputPeerChat chat :
await this . Messages_EditChatAdmin ( chat . chat_id , user , is_admin ) ;
return new Updates { date = DateTime . UtcNow , users = new ( ) , updates = Array . Empty < Update > ( ) ,
chats = ( await this . Messages_GetChats ( new [ ] { chat . chat_id } ) ) . chats } ;
case InputPeerChannel channel :
2022-01-11 04:14:23 +01:00
return await this . Channels_EditAdmin ( channel , user ,
2021-12-31 08:38:41 +01:00
new ChatAdminRights { flags = is_admin ? ( ChatAdminRights . Flags ) 0x8BF : 0 } , null ) ;
default :
throw new ArgumentException ( "This method works on Chat & Channel only" ) ;
}
}
public Task < UpdatesBase > EditChatPhoto ( InputPeer peer , InputChatPhotoBase photo ) = > peer switch
{
InputPeerChat chat = > this . Messages_EditChatPhoto ( chat . chat_id , photo ) ,
InputPeerChannel channel = > this . Channels_EditPhoto ( channel , photo ) ,
_ = > throw new ArgumentException ( "This method works on Chat & Channel only" ) ,
} ;
public Task < UpdatesBase > EditChatTitle ( InputPeer peer , string title ) = > peer switch
{
InputPeerChat chat = > this . Messages_EditChatTitle ( chat . chat_id , title ) ,
InputPeerChannel channel = > this . Channels_EditTitle ( channel , title ) ,
_ = > throw new ArgumentException ( "This method works on Chat & Channel only" ) ,
} ;
2021-12-30 17:37:25 +01:00
public Task < Messages_ChatFull > GetFullChat ( InputPeer peer ) = > peer switch
2021-08-30 01:31:08 +02:00
{
2021-12-30 17:37:25 +01:00
InputPeerChat chat = > this . Messages_GetFullChat ( chat . chat_id ) ,
InputPeerChannel channel = > this . Channels_GetFullChannel ( channel ) ,
_ = > throw new ArgumentException ( "This method works on Chat & Channel only" ) ,
} ;
2021-12-31 08:38:41 +01:00
public async Task < UpdatesBase > DeleteChat ( InputPeer peer )
{
switch ( peer )
{
case InputPeerChat chat :
await this . Messages_DeleteChat ( chat . chat_id ) ;
return new Updates { date = DateTime . UtcNow , users = new ( ) , updates = Array . Empty < Update > ( ) ,
chats = ( await this . Messages_GetChats ( new [ ] { chat . chat_id } ) ) . chats } ;
case InputPeerChannel channel :
return await this . Channels_DeleteChannel ( channel ) ;
default :
throw new ArgumentException ( "This method works on Chat & Channel only" ) ;
}
}
2021-12-30 17:37:25 +01:00
#endregion
2021-08-04 00:40:09 +02:00
}
}