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 ;
using System.Diagnostics ;
using System.Globalization ;
using System.IO ;
using System.Linq ;
using System.Net ;
using System.Net.Sockets ;
using System.Reflection ;
using System.Security.Cryptography ;
using System.Text ;
using System.Threading.Tasks ;
using TL ;
using static WTelegram . Encryption ;
2021-08-05 22:38:46 +02:00
//TODO: include XML comments in nuget
2021-08-04 00:40:09 +02:00
namespace WTelegram
{
public sealed class Client : IDisposable
{
public Config TLConfig { get ; private set ; }
private readonly Func < string , string > _config ;
private readonly Func < ITLObject , Task > _updateHandler ;
private readonly int _apiId ;
private readonly string _apiHash ;
private readonly Session _session ;
private TcpClient _tcpClient ;
private NetworkStream _networkStream ;
private int _frame_seqTx = 0 , _frame_seqRx = 0 ;
private ITLObject _lastSentMsg ;
private Type _lastRpcResultType = typeof ( object ) ;
private readonly List < long > _msgsToAck = new ( ) ;
private int _unexpectedSaltChange ;
public Client ( Func < string , string > configProvider = null , Func < ITLObject , Task > updateHandler = null )
{
_config = configProvider ? ? DefaultConfigOrAsk ;
_updateHandler = updateHandler ;
2021-08-04 12:03:43 +02:00
_apiId = int . Parse ( Config ( "api_id" ) ) ;
_apiHash = Config ( "api_hash" ) ;
_session = Session . LoadOrCreate ( Config ( "session_pathname" ) ) ;
2021-08-04 00:40:09 +02:00
}
2021-08-04 12:03:43 +02:00
public string Config ( string config )
= > _config ( config ) ? ? DefaultConfig ( config ) ? ? throw new ApplicationException ( "You must provide a config value for " + config ) ;
2021-08-04 00:40:09 +02:00
public static string DefaultConfig ( string config ) = > config switch
{
"session_pathname" = > Path . Combine (
Path . GetDirectoryName ( Path . GetDirectoryName ( AppDomain . CurrentDomain . BaseDirectory . TrimEnd ( Path . DirectorySeparatorChar ) ) ) ,
"WTelegram.session" ) ,
#if DEBUG
"server_address" = > "149.154.167.40:443" ,
#else
"server_address" = > "149.154.167.50:443" ,
#endif
"device_model" = > Environment . Is64BitOperatingSystem ? "PC 64bit" : "PC 32bit" ,
"system_version" = > System . Runtime . InteropServices . RuntimeInformation . OSDescription ,
"app_version" = > Assembly . GetEntryAssembly ( ) . GetName ( ) . Version . ToString ( ) ,
"system_lang_code" = > CultureInfo . InstalledUICulture . TwoLetterISOLanguageName ,
"lang_pack" = > "" ,
"lang_code" = > CultureInfo . CurrentUICulture . TwoLetterISOLanguageName ,
2021-08-04 12:03:43 +02:00
_ = > null // api_id api_hash phone_number verification_code... it's up to you to reply to these correctly
2021-08-04 00:40:09 +02:00
} ;
2021-08-05 16:29:58 +02:00
public static string DefaultConfigOrAsk ( string config )
2021-08-04 00:40:09 +02:00
{
var value = DefaultConfig ( config ) ;
if ( value ! = null ) return value ;
Console . Write ( $"Enter {config.Replace('_', ' ')}: " ) ;
return Console . ReadLine ( ) ;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822")]
public void LoadPublicKey ( string pem ) = > Encryption . LoadPublicKey ( pem ) ;
public async Task ConnectAsync ( )
{
2021-08-04 12:03:43 +02:00
var endpoint = _session . DataCenter = = null ? IPEndPoint . Parse ( Config ( "server_address" ) )
2021-08-04 00:40:09 +02:00
: new IPEndPoint ( IPAddress . Parse ( _session . DataCenter . ip_address ) , _session . DataCenter . port ) ;
2021-08-04 10:11:07 +02:00
Helpers . Log ( 2 , $"Connecting to {endpoint}..." ) ;
2021-08-04 00:40:09 +02:00
_tcpClient = new TcpClient ( endpoint . AddressFamily ) ;
await _tcpClient . ConnectAsync ( endpoint . Address , endpoint . Port ) ;
_networkStream = _tcpClient . GetStream ( ) ;
_frame_seqTx = _frame_seqRx = 0 ;
if ( _session . AuthKey = = null )
await CreateAuthorizationKey ( this , _session ) ;
TLConfig = await CallAsync ( new InvokeWithLayer < Config >
{
layer = Schema . Layer ,
query = new InitConnection < Config >
{
api_id = _apiId ,
2021-08-04 12:03:43 +02:00
device_model = Config ( "device_model" ) ,
system_version = Config ( "system_version" ) ,
app_version = Config ( "app_version" ) ,
system_lang_code = Config ( "system_lang_code" ) ,
lang_pack = Config ( "lang_pack" ) ,
lang_code = Config ( "lang_code" ) ,
2021-08-04 00:40:09 +02:00
query = new Help_GetConfig ( )
}
} ) ;
}
private async Task MigrateDCAsync ( int dcId )
{
2021-08-04 10:11:07 +02:00
Helpers . Log ( 2 , $"Migrate to DC {dcId}..." ) ;
2021-08-04 00:40:09 +02:00
//TODO: Export/Import client authorization?
var prevFamily = _tcpClient . Client . RemoteEndPoint . AddressFamily ;
_tcpClient . Close ( ) ;
var dcOptions = TLConfig . dc_options . Where ( dc = > dc . id = = dcId & & ( dc . flags & ( DcOption . Flags . media_only | DcOption . Flags . cdn ) ) = = 0 ) ;
if ( prevFamily = = AddressFamily . InterNetworkV6 ) // try to stay in the same connectivity
dcOptions = dcOptions . OrderByDescending ( dc = > dc . flags & DcOption . Flags . ipv6 ) ; // list ipv6 first
else
dcOptions = dcOptions . OrderBy ( dc = > dc . flags & DcOption . Flags . ipv6 ) ; // list ipv4 first
var dcOption = dcOptions . FirstOrDefault ( ) ;
_session . DataCenter = dcOption ? ? throw new ApplicationException ( $"Could not find adequate dcOption for DC {dcId}" ) ;
_session . AuthKeyID = _session . Salt = _session . Seqno = 0 ;
_session . AuthKey = null ;
await ConnectAsync ( ) ;
}
public void Dispose ( )
{
CheckMsgsToAck ( ) . Wait ( 1000 ) ;
_networkStream ? . Dispose ( ) ;
_tcpClient ? . Dispose ( ) ;
}
public async Task SendAsync ( ITLObject msg , bool isContent = true )
{
if ( _session . AuthKeyID ! = 0 ) await CheckMsgsToAck ( ) ;
using var memStream = new MemoryStream ( 1024 ) ; //TODO: choose a useful capacity
using var writer = new BinaryWriter ( memStream , Encoding . UTF8 ) ;
writer . Write ( 0 ) ; // int32 frame_len (to be patched with full frame length)
writer . Write ( _frame_seqTx + + ) ; // int32 frame_seq
( long msgId , int seqno ) = _session . NewMsg ( isContent ) ;
if ( _session . AuthKeyID = = 0 ) // send unencrypted message
{
2021-08-04 10:11:07 +02:00
Helpers . Log ( 1 , $"Sending {msg.GetType().Name}..." ) ;
2021-08-04 00:40:09 +02:00
writer . Write ( 0L ) ; // int64 auth_key_id = 0 (Unencrypted)
writer . Write ( msgId ) ; // int64 message_id
writer . Write ( 0 ) ; // int32 message_data_length (to be patched)
Schema . Serialize ( writer , msg ) ; // bytes message_data
2021-08-06 01:54:29 +02:00
BinaryPrimitives . WriteInt32LittleEndian ( memStream . GetBuffer ( ) . AsSpan ( 24 ) , ( int ) memStream . Length - 28 ) ; // patch message_data_length
2021-08-04 00:40:09 +02:00
}
else
{
2021-08-06 01:54:29 +02:00
Helpers . Log ( 1 , $"Sending {msg.GetType().Name,-50} #{(short)msgId.GetHashCode():X4}" ) ;
2021-08-04 00:40:09 +02:00
//TODO: Implement MTProto 2.0
using var clearStream = new MemoryStream ( 1024 ) ; //TODO: choose a useful capacity
using var clearWriter = new BinaryWriter ( clearStream , Encoding . UTF8 ) ;
clearWriter . Write ( _session . Salt ) ; // int64 salt
clearWriter . Write ( _session . Id ) ; // int64 session_id
clearWriter . Write ( msgId ) ; // int64 message_id
clearWriter . Write ( seqno ) ; // int32 msg_seqno
clearWriter . Write ( 0 ) ; // int32 message_data_length (to be patched)
Schema . Serialize ( clearWriter , msg ) ; // bytes message_data
int clearLength = ( int ) clearStream . Length ; // length before padding (= 32 + message_data_length)
int padding = ( 0x7FFFFFF0 - clearLength ) % 16 ;
clearStream . SetLength ( clearLength + padding ) ;
byte [ ] clearBuffer = clearStream . GetBuffer ( ) ;
2021-08-06 01:54:29 +02:00
BinaryPrimitives . WriteInt32LittleEndian ( clearBuffer . AsSpan ( 28 ) , clearLength - 32 ) ; // patch message_data_length
2021-08-04 00:40:09 +02:00
RNG . GetBytes ( clearBuffer , clearLength , padding ) ;
var clearSha1 = SHA1 . HashData ( clearBuffer . AsSpan ( 0 , clearLength ) ) ; // padding excluded from computation!
byte [ ] encrypted_data = EncryptDecryptMessage ( clearBuffer . AsSpan ( 0 , clearLength + padding ) , true , _session . AuthKey , clearSha1 ) ;
writer . Write ( _session . AuthKeyID ) ; // int64 auth_key_id
writer . Write ( clearSha1 , 4 , 16 ) ; // int128 msg_key = low 128-bits of SHA1(clear message body)
writer . Write ( encrypted_data ) ; // bytes encrypted_data
}
var buffer = memStream . GetBuffer ( ) ;
int frameLength = ( int ) memStream . Length ;
//TODO: support Quick Ack?
2021-08-06 01:54:29 +02:00
BinaryPrimitives . WriteInt32LittleEndian ( buffer , frameLength + 4 ) ; // patch frame_len with correct value
2021-08-04 00:40:09 +02:00
uint crc = Force . Crc32 . Crc32Algorithm . Compute ( buffer , 0 , frameLength ) ;
writer . Write ( crc ) ; // int32 frame_crc
var frame = memStream . GetBuffer ( ) . AsMemory ( 0 , frameLength + 4 ) ;
//TODO: support Transport obfuscation?
await _networkStream . WriteAsync ( frame ) ;
_lastSentMsg = msg ;
}
2021-08-04 10:11:07 +02:00
internal async Task < ITLObject > RecvInternalAsync ( )
2021-08-04 00:40:09 +02:00
{
var data = await RecvFrameAsync ( ) ;
if ( data . Length = = 4 & & data [ 3 ] = = 0xFF )
2021-08-06 01:54:29 +02:00
throw new ApplicationException ( $"Server replied with error code: {TransportError(-BinaryPrimitives.ReadInt32LittleEndian(data))}" ) ;
2021-08-04 00:40:09 +02:00
if ( data . Length < 24 ) // authKeyId+msgId+length+ctorNb | authKeyId+msgKey
throw new ApplicationException ( $"Packet payload too small: {data.Length}" ) ;
//TODO: ignore msgId <= lastRecvMsgId, ignore MsgId >= 30 sec in the future or < 300 sec in the past
2021-08-06 01:54:29 +02:00
long authKeyId = BinaryPrimitives . ReadInt64LittleEndian ( data ) ;
2021-08-04 00:40:09 +02:00
if ( authKeyId = = 0 ) // Unencrypted message
{
using var reader = new BinaryReader ( new MemoryStream ( data , 8 , data . Length - 8 ) ) ;
long msgId = reader . ReadInt64 ( ) ;
if ( ( msgId & 1 ) = = 0 ) throw new ApplicationException ( $"Invalid server msgId {msgId}" ) ;
int length = reader . ReadInt32 ( ) ;
if ( length ! = data . Length - 20 ) throw new ApplicationException ( $"Unexpected unencrypted length {length} != {data.Length - 20}" ) ;
var ctorNb = reader . ReadUInt32 ( ) ;
if ( ! Schema . Mappings . TryGetValue ( ctorNb , out var realType ) )
throw new ApplicationException ( $"Cannot find type for ctor #{ctorNb:x}" ) ;
2021-08-04 10:11:07 +02:00
Helpers . Log ( 1 , $"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} unencrypted" ) ;
2021-08-04 00:40:09 +02:00
return Schema . DeserializeObject ( reader , realType ) ;
}
else if ( authKeyId ! = _session . AuthKeyID )
throw new ApplicationException ( $"Received a packet encrypted with unexpected key {authKeyId:X}" ) ;
else
{
byte [ ] decrypted_data = EncryptDecryptMessage ( data . AsSpan ( 24 ) , false , _session . AuthKey , data [ 4. . 24 ] ) ;
if ( decrypted_data . Length < 36 ) // header below+ctorNb
throw new ApplicationException ( $"Decrypted packet too small: {decrypted_data.Length}" ) ;
using var reader = new BinaryReader ( new MemoryStream ( decrypted_data ) ) ;
var serverSalt = reader . ReadInt64 ( ) ; // int64 salt
var sessionId = reader . ReadInt64 ( ) ; // int64 session_id
var msgId = reader . ReadInt64 ( ) ; // int64 message_id
var seqno = reader . ReadInt32 ( ) ; // int32 msg_seqno
var length = reader . ReadInt32 ( ) ; // int32 message_data_length
if ( serverSalt ! = _session . Salt )
2021-08-05 16:29:58 +02:00
{
Helpers . Log ( 3 , $"Server salt has changed: {_session.Salt:X8} -> {serverSalt:X8}" ) ;
_session . Salt = serverSalt ;
2021-08-04 00:40:09 +02:00
if ( + + _unexpectedSaltChange > = 10 )
throw new ApplicationException ( $"Server salt changed unexpectedly more than 10 times during this run" ) ;
2021-08-05 16:29:58 +02:00
}
2021-08-04 00:40:09 +02:00
if ( sessionId ! = _session . Id ) throw new ApplicationException ( $"Unexpected session ID {_session.Id} != {_session.Id}" ) ;
if ( ( msgId & 1 ) = = 0 ) throw new ApplicationException ( $"Invalid server msgId {msgId}" ) ;
if ( ( seqno & 1 ) ! = 0 ) lock ( _msgsToAck ) _msgsToAck . Add ( msgId ) ;
if ( decrypted_data . Length - 32 - length is < 0 or > 15 ) throw new ApplicationException ( $"Unexpected decrypted message_data_length {length} / {decrypted_data.Length - 32}" ) ;
if ( ! data . AsSpan ( 8 , 16 ) . SequenceEqual ( SHA1 . HashData ( decrypted_data . AsSpan ( 0 , 32 + length ) ) . AsSpan ( 4 ) ) )
throw new ApplicationException ( $"Mismatch between MsgKey & decrypted SHA1" ) ;
var ctorNb = reader . ReadUInt32 ( ) ;
if ( ! Schema . Mappings . TryGetValue ( ctorNb , out var realType ) )
throw new ApplicationException ( $"Cannot find type for ctor #{ctorNb:x}" ) ;
2021-08-04 10:11:07 +02:00
Helpers . Log ( 1 , $"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} {(seqno == -1 ? " clearText " : " isContent ")}={(seqno & 1) != 0}" ) ;
2021-08-04 00:40:09 +02:00
if ( realType = = typeof ( RpcResult ) )
return DeserializeRpcResult ( reader ) ; // hack necessary because some RPC return bare types like bool or int[]
else
return Schema . DeserializeObject ( reader , realType ) ;
}
static string TransportError ( int error_code ) = > error_code switch
{
404 = > "Auth key not found" ,
429 = > "Transport flood" ,
_ = > ( ( HttpStatusCode ) error_code ) . ToString ( ) ,
} ;
}
private async Task < byte [ ] > RecvFrameAsync ( )
{
byte [ ] frame = new byte [ 8 ] ;
2021-08-05 16:29:58 +02:00
if ( await FullReadAsync ( _networkStream , frame , 8 ) ! = 8 )
throw new ApplicationException ( "Could not read frame prefix : Connection shut down" ) ;
2021-08-06 01:54:29 +02:00
int length = BinaryPrimitives . ReadInt32LittleEndian ( frame ) - 12 ;
2021-08-04 00:40:09 +02:00
if ( length < = 0 | | length > = 0x10000 )
throw new ApplicationException ( "Invalid frame_len" ) ;
2021-08-06 01:54:29 +02:00
int seqno = BinaryPrimitives . ReadInt32LittleEndian ( frame . AsSpan ( 4 ) ) ;
2021-08-04 00:40:09 +02:00
if ( seqno ! = _frame_seqRx + + )
{
Trace . TraceWarning ( $"Unexpected frame_seq received: {seqno} instead of {_frame_seqRx}" ) ;
_frame_seqRx = seqno + 1 ;
}
var payload = new byte [ length ] ;
2021-08-05 16:29:58 +02:00
if ( await FullReadAsync ( _networkStream , payload , length ) ! = length )
throw new ApplicationException ( "Could not read frame data : Connection shut down" ) ;
2021-08-04 00:40:09 +02:00
uint crc32 = Force . Crc32 . Crc32Algorithm . Compute ( frame , 0 , 8 ) ;
crc32 = Force . Crc32 . Crc32Algorithm . Append ( crc32 , payload ) ;
2021-08-05 16:29:58 +02:00
if ( await FullReadAsync ( _networkStream , frame , 4 ) ! = 4 )
throw new ApplicationException ( "Could not read frame CRC : Connection shut down" ) ;
2021-08-06 01:54:29 +02:00
if ( crc32 ! = BinaryPrimitives . ReadUInt32LittleEndian ( frame ) )
2021-08-04 00:40:09 +02:00
throw new ApplicationException ( "Invalid envelope CRC32" ) ;
return payload ;
}
2021-08-05 16:29:58 +02:00
private static async Task < int > FullReadAsync ( Stream stream , byte [ ] buffer , int length )
2021-08-04 00:40:09 +02:00
{
for ( int offset = 0 ; offset ! = length ; )
{
2021-08-05 16:29:58 +02:00
var read = await stream . ReadAsync ( buffer . AsMemory ( offset , length - offset ) ) ;
if ( read = = 0 ) return offset ;
2021-08-04 00:40:09 +02:00
offset + = read ;
2021-08-05 16:29:58 +02:00
}
return length ;
2021-08-04 00:40:09 +02:00
}
2021-08-04 10:11:07 +02:00
private RpcResult DeserializeRpcResult ( BinaryReader reader )
2021-08-04 00:40:09 +02:00
{
long reqMsgId = reader . ReadInt64 ( ) ;
var rpcResult = new RpcResult { req_msg_id = reqMsgId } ;
if ( reqMsgId = = _session . LastSentMsgId )
rpcResult . result = Schema . DeserializeValue ( reader , _lastRpcResultType ) ;
else
2021-08-04 10:11:07 +02:00
rpcResult . result = Schema . Deserialize < ITLObject > ( reader ) ;
2021-08-06 01:54:29 +02:00
Helpers . Log ( 1 , $" → {rpcResult.result.GetType().Name,-50} #{(short)reqMsgId.GetHashCode():X4}" ) ;
2021-08-04 00:40:09 +02:00
return rpcResult ;
}
public class RpcException : Exception
{
public readonly int Code ;
public RpcException ( int code , string message ) : base ( message ) = > Code = code ;
}
public async Task < X > CallAsync < X > ( ITLFunction < X > request )
{
await SendAsync ( request ) ;
_lastRpcResultType = typeof ( X ) ;
for ( ; ; )
{
var reply = await RecvInternalAsync ( ) ;
if ( reply is RpcResult rpcResult & & rpcResult . req_msg_id = = _session . LastSentMsgId )
{
if ( rpcResult . result is RpcError rpcError )
{
int migrateDC ;
if ( rpcError . error_code = = 303 & & ( ( migrateDC = rpcError . error_message . IndexOf ( "_MIGRATE_" ) ) > 0 ) )
{
migrateDC = int . Parse ( rpcError . error_message [ ( migrateDC + 9 ) . . ] ) ;
await MigrateDCAsync ( migrateDC ) ;
await SendAsync ( request ) ;
}
else
throw new RpcException ( rpcError . error_code , rpcError . error_message ) ;
}
else if ( rpcResult . result is X result )
return result ;
else
throw new ApplicationException ( $"{request.GetType().Name} call got a result of type {rpcResult.result.GetType().Name} instead of {typeof(X).Name}" ) ;
}
else
await HandleMessageAsync ( reply ) ;
}
}
private async Task CheckMsgsToAck ( )
{
MsgsAck msgsAck = null ;
lock ( _msgsToAck )
if ( _msgsToAck . Count ! = 0 )
{
msgsAck = new MsgsAck { msg_ids = _msgsToAck . ToArray ( ) } ;
_msgsToAck . Clear ( ) ;
}
if ( msgsAck ! = null )
await SendAsync ( msgsAck , false ) ;
}
2021-08-04 10:11:07 +02:00
private async Task HandleMessageAsync ( ITLObject obj )
2021-08-04 00:40:09 +02:00
{
switch ( obj )
{
case MsgContainer container :
foreach ( var msg in container . messages )
{
2021-08-04 10:11:07 +02:00
Helpers . Log ( 1 , $" → {msg.body?.GetType().Name,-48} timestamp={_session.MsgIdToStamp(msg.msg_id)} isResponse={(msg.msg_id & 2) != 0} {(msg.seqno == -1 ? " clearText " : " isContent ")}={(msg.seqno & 1) != 0}" ) ;
2021-08-04 00:40:09 +02:00
if ( ( msg . seqno & 1 ) ! = 0 ) lock ( _msgsToAck ) _msgsToAck . Add ( msg . msg_id ) ;
if ( msg . body ! = null ) await HandleMessageAsync ( msg . body ) ;
}
break ;
case BadServerSalt badServerSalt :
_session . Salt = badServerSalt . new_server_salt ;
if ( badServerSalt . bad_msg_id = = _session . LastSentMsgId )
await SendAsync ( _lastSentMsg ) ;
break ;
case BadMsgNotification badMsgNotification :
2021-08-04 10:11:07 +02:00
Helpers . Log ( 3 , $"BadMsgNotification {badMsgNotification.error_code} for msg {badMsgNotification.bad_msg_seqno}" ) ;
2021-08-04 00:40:09 +02:00
break ;
case RpcResult rpcResult :
if ( _session . MsgIdToStamp ( rpcResult . req_msg_id ) > = _session . SessionStart )
throw new ApplicationException ( $"Got RpcResult({rpcResult.result.GetType().Name}) for unknown msgId {rpcResult.req_msg_id}" ) ;
break ; // silently ignore results for msg_id from previous sessions
default :
2021-08-04 10:11:07 +02:00
_updateHandler ? . Invoke ( obj ) ;
2021-08-04 00:40:09 +02:00
break ;
}
}
public async Task < User > UserAuthIfNeeded ( CodeSettings settings = null )
{
if ( _session . User ! = null )
return Schema . Deserialize < User > ( _session . User ) ;
2021-08-04 12:03:43 +02:00
string phone_number = Config ( "phone_number" ) ;
2021-08-04 00:40:09 +02:00
var sentCode = await CallAsync ( new Auth_SendCode
{
phone_number = phone_number ,
api_id = _apiId ,
api_hash = _apiHash ,
settings = settings ? ? new ( )
} ) ;
2021-08-04 10:11:07 +02:00
Helpers . Log ( 3 , $"A verification code has been sent via {sentCode.type.GetType().Name[17..]}" ) ;
2021-08-04 12:03:43 +02:00
var verification_code = Config ( "verification_code" ) ;
2021-08-04 00:40:09 +02:00
var authorization = await CallAsync ( new Auth_SignIn
{
phone_number = phone_number ,
phone_code_hash = sentCode . phone_code_hash ,
phone_code = verification_code
} ) ;
if ( authorization is not Auth_Authorization { user : User user } auth_success )
throw new ApplicationException ( "Failed to get Authorization: " + authorization . GetType ( ) . Name ) ;
//TODO: support Auth_AuthorizationSignUpRequired?
_session . User = Schema . Serialize ( user ) ;
return user ;
}
2021-08-05 16:29:58 +02:00
#region TL - Helpers
/// <summary>Helper function to upload a file to Telegram</summary>
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
public Task < InputFileBase > UploadFileAsync ( string pathname )
= > UploadFileAsync ( File . OpenRead ( pathname ) , Path . GetFileName ( pathname ) ) ;
public async Task < InputFileBase > UploadFileAsync ( Stream stream , string filename )
{
using var md5 = MD5 . Create ( ) ;
using ( stream )
{
long length = stream . Length ;
var isBig = length > = 10 * 1024 * 1024 ;
const int partSize = 512 * 1024 ;
int file_total_parts = ( int ) ( ( length - 1 ) / partSize ) + 1 ;
long file_id = Helpers . RandomLong ( ) ;
var bytes = new byte [ Math . Min ( partSize , length ) ] ;
int file_part = 0 , read ;
for ( long bytesLeft = length ; bytesLeft ! = 0 ; file_part + + )
{
// TODO: parallelize several parts sending through a N-semaphore?
read = await FullReadAsync ( stream , bytes , ( int ) Math . Min ( partSize , bytesLeft ) ) ;
await CallAsync < bool > ( isBig
? new Upload_SaveBigFilePart { bytes = bytes , file_id = file_id , file_part = file_part , file_total_parts = file_total_parts }
: new Upload_SaveFilePart { bytes = bytes , file_id = file_id , file_part = file_part } ) ;
if ( ! isBig ) md5 . TransformBlock ( bytes , 0 , read , null , 0 ) ;
bytesLeft - = read ;
if ( read < partSize & & bytesLeft ! = 0 ) throw new ApplicationException ( $"Failed to fully read stream ({read},{bytesLeft})" ) ;
}
if ( ! isBig ) md5 . TransformFinalBlock ( bytes , 0 , 0 ) ;
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 } ;
}
}
/// <summary>Helper function to send a text or media message more easily</summary>
/// <param name="peer">destination of message</param>
/// <param name="caption">media caption</param>
/// <param name="mediaFile"><see langword="null"/> or a media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
/// <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>
public Task < UpdatesBase > SendMediaAsync ( InputPeer peer , string caption , InputFileBase mediaFile , string mimeType = null , int reply_to_msg_id = 0 , MessageEntity [ ] entities = null , DateTime schedule_date = default , bool disable_preview = false )
{
var filename = mediaFile is InputFile iFile ? iFile . name : ( mediaFile as InputFileBig ) ? . name ;
mimeType ? ? = Path . GetExtension ( filename ) . ToLowerInvariant ( ) switch
{
".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 } ,
reply_to_msg_id , entities , schedule_date , disable_preview ) ;
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
} , reply_to_msg_id , entities , schedule_date , disable_preview ) ;
}
/// <summary>Helper function to send a text or media message</summary>
/// <param name="peer">destination of message</param>
/// <param name="text">text, or media caption</param>
/// <param name="media">media specification or <see langword="null"/></param>
public Task < UpdatesBase > SendMessageAsync ( InputPeer peer , string text , InputMedia media = null , int reply_to_msg_id = 0 , MessageEntity [ ] entities = null , DateTime schedule_date = default , bool disable_preview = false )
{
ITLFunction < UpdatesBase > request = ( media = = null )
? new Messages_SendMessage
{
flags = GetFlags ( ) ,
peer = peer ,
reply_to_msg_id = reply_to_msg_id ,
message = text ,
random_id = Helpers . RandomLong ( ) ,
entities = entities ,
schedule_date = schedule_date
}
: new Messages_SendMedia
{
flags = ( Messages_SendMedia . Flags ) GetFlags ( ) ,
peer = peer ,
reply_to_msg_id = reply_to_msg_id ,
media = media ,
message = text ,
random_id = Helpers . RandomLong ( ) ,
entities = entities ,
schedule_date = schedule_date
} ;
return CallAsync ( request ) ;
Messages_SendMessage . Flags GetFlags ( )
{
return ( ( reply_to_msg_id ! = 0 ) ? Messages_SendMessage . Flags . has_reply_to_msg_id : 0 )
| ( disable_preview ? Messages_SendMessage . Flags . no_webpage : 0 )
// | (reply_markup != null ? Messages_SendMessage.Flags.has_reply_markup : 0)
| ( entities ! = null ? Messages_SendMessage . Flags . has_entities : 0 )
| ( schedule_date ! = default ? Messages_SendMessage . Flags . has_schedule_date : 0 ) ;
}
}
#endregion
2021-08-04 00:40:09 +02:00
}
}