2022-04-06 18:38:54 +02:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Reflection ;
using System.Security.Cryptography ;
using System.Threading ;
using System.Threading.Tasks ;
using TL ;
// necessary for .NET Standard 2.0 compilation:
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
namespace WTelegram
{
partial class Client
{
#region Client TL Helpers
2022-07-04 12:55:00 +02:00
/// <summary>Used to indicate progression of file download/upload</summary>
/// <param name="transmitted">transmitted bytes</param>
/// <param name="totalSize">total size of file in bytes, or 0 if unknown</param>
public delegate void ProgressCallback ( long transmitted , long totalSize ) ;
2022-04-06 18:38:54 +02:00
/// <summary>Helper function to upload a file to Telegram</summary>
/// <param name="pathname">Path to the file to upload</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
public Task < InputFileBase > UploadFileAsync ( string pathname , ProgressCallback progress = null )
= > UploadFileAsync ( File . OpenRead ( pathname ) , Path . GetFileName ( pathname ) , progress ) ;
/// <summary>Helper function to upload a file to Telegram</summary>
/// <param name="stream">Content of the file to upload. This method close/dispose the stream</param>
/// <param name="filename">Name of the file</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
public async Task < InputFileBase > UploadFileAsync ( Stream stream , string filename , ProgressCallback progress = null )
{
using var md5 = MD5 . Create ( ) ;
using ( stream )
{
long transmitted = 0 , length = stream . Length ;
var isBig = length > = 10 * 1024 * 1024 ;
int file_total_parts = ( int ) ( ( length - 1 ) / FilePartSize ) + 1 ;
long file_id = Helpers . RandomLong ( ) ;
int file_part = 0 , read ;
var tasks = new Dictionary < int , Task > ( ) ;
bool abort = false ;
for ( long bytesLeft = length ; ! abort & & bytesLeft ! = 0 ; file_part + + )
{
2022-10-26 17:33:06 +02:00
var bytes = new byte [ Math . Min ( FilePartSize , bytesLeft ) ] ;
2022-04-06 18:38:54 +02:00
read = await stream . FullReadAsync ( bytes , bytes . Length , default ) ;
await _parallelTransfers . WaitAsync ( ) ;
bytesLeft - = read ;
var task = SavePart ( file_part , bytes ) ;
lock ( tasks ) tasks [ file_part ] = task ;
if ( ! isBig )
md5 . TransformBlock ( bytes , 0 , read , null , 0 ) ;
2023-04-02 13:44:23 +02:00
if ( read < FilePartSize & & bytesLeft ! = 0 ) throw new WTException ( $"Failed to fully read stream ({read},{bytesLeft})" ) ;
2022-04-06 18:38:54 +02:00
async Task SavePart ( int file_part , byte [ ] bytes )
{
try
{
if ( isBig )
await this . Upload_SaveBigFilePart ( file_id , file_part , file_total_parts , bytes ) ;
else
await this . Upload_SaveFilePart ( file_id , file_part , bytes ) ;
lock ( tasks ) { transmitted + = bytes . Length ; tasks . Remove ( file_part ) ; }
progress ? . Invoke ( transmitted , length ) ;
}
catch ( Exception )
{
abort = true ;
throw ;
}
finally
{
_parallelTransfers . Release ( ) ;
}
}
}
Task [ ] remainingTasks ;
lock ( tasks ) remainingTasks = tasks . Values . ToArray ( ) ;
await Task . WhenAll ( remainingTasks ) ; // wait completion and eventually propagate any task exception
if ( ! isBig ) md5 . TransformFinalBlock ( Array . Empty < byte > ( ) , 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 } ;
}
}
2022-06-23 01:49:07 +02:00
/// <summary>Search messages in chat with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.search"/></para></summary>
2022-04-06 18:38:54 +02:00
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
/// <param name="peer">User or chat, histories with which are searched, or <see langword="null"/> constructor for global search</param>
/// <param name="text">Text search request</param>
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
public Task < Messages_MessagesBase > Messages_Search < T > ( InputPeer peer , string text = null , int offset_id = 0 , int limit = int . MaxValue ) where T : MessagesFilter , new ( )
= > this . Messages_Search ( peer , text , new T ( ) , offset_id : offset_id , limit : limit ) ;
2022-06-23 01:49:07 +02:00
/// <summary>Search messages globally with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.searchGlobal"/></para></summary>
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
/// <param name="text">Text search request</param>
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
public Task < Messages_MessagesBase > Messages_SearchGlobal < T > ( string text = null , int offset_id = 0 , int limit = int . MaxValue ) where T : MessagesFilter , new ( )
= > this . Messages_SearchGlobal ( text , new T ( ) , offset_id : offset_id , limit : limit ) ;
2022-04-06 18:38:54 +02:00
/// <summary>Helper function to send a media message more easily</summary>
/// <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>
/// <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>
/// <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>
/// <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 )
{
mimeType ? ? = Path . GetExtension ( mediaFile . Name ) ? . 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 ) ;
return SendMessageAsync ( peer , caption , new InputMediaUploadedDocument ( mediaFile , mimeType ) , reply_to_msg_id , entities , schedule_date ) ;
}
/// <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>
2022-08-11 19:39:18 +02:00
/// <param name="entities">Text formatting entities. You can use <see cref="HtmlText.HtmlToEntities">HtmlToEntities</see> or <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
2022-04-06 18:38:54 +02:00
/// <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>
/// <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 )
{
UpdatesBase updates ;
long random_id = Helpers . RandomLong ( ) ;
if ( media = = null )
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 ) ;
else
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 ) ;
2022-07-29 15:24:18 +02:00
RaiseUpdate ( updates ) ;
2022-04-06 18:38:54 +02:00
int msgId = - 1 ;
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 )
} ;
2023-04-29 16:45:03 +02:00
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 ;
}
2022-04-06 18:38:54 +02:00
}
return null ;
}
/// <summary>Helper function to send an album (media group) of photos or documents more easily</summary>
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
2022-10-12 23:25:15 +02:00
/// <param name="medias">An array or List of <see cref="InputMedia">InputMedia</see>-derived class</param>
2022-04-06 18:38:54 +02:00
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></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 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>
/// <returns>The last of the media group messages, confirmed by Telegram</returns>
/// <remarks>
/// * The caption/entities are set on the last media<br/>
/// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported by downloading the file from the web via HttpClient and sending it to Telegram.
/// WTelegramClient proxy settings don't apply to HttpClient<br/>
/// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes
/// </remarks>
2022-10-12 23:25:15 +02:00
public async Task < Message [ ] > SendAlbumAsync ( InputPeer peer , ICollection < InputMedia > medias , string caption = null , int reply_to_msg_id = 0 , MessageEntity [ ] entities = null , DateTime schedule_date = default )
2022-04-06 18:38:54 +02:00
{
System . Net . Http . HttpClient httpClient = null ;
2022-10-12 23:25:15 +02:00
int i = 0 , length = medias . Count ;
var multiMedia = new InputSingleMedia [ length ] ;
2022-07-24 11:14:08 +02:00
var random_id = Helpers . RandomLong ( ) ;
2022-10-12 23:25:15 +02:00
foreach ( var media in medias )
2022-04-06 18:38:54 +02:00
{
2022-10-12 23:25:15 +02:00
var ism = multiMedia [ i ] = new InputSingleMedia { random_id = random_id + i , media = media } ;
i + + ;
2022-04-06 18:38:54 +02:00
retry :
switch ( ism . media )
{
case InputMediaUploadedPhoto imup :
var mmp = ( MessageMediaPhoto ) await this . Messages_UploadMedia ( peer , imup ) ;
ism . media = mmp . photo ;
break ;
case InputMediaUploadedDocument imud :
var mmd = ( MessageMediaDocument ) await this . Messages_UploadMedia ( peer , imud ) ;
ism . media = mmd . document ;
break ;
case InputMediaDocumentExternal imde :
string mimeType = null ;
var inputFile = await UploadFromUrl ( imde . url ) ;
ism . media = new InputMediaUploadedDocument ( inputFile , mimeType ) ;
goto retry ;
case InputMediaPhotoExternal impe :
inputFile = await UploadFromUrl ( impe . url ) ;
ism . media = new InputMediaUploadedPhoto { file = inputFile } ;
goto retry ;
async Task < InputFileBase > UploadFromUrl ( string url )
{
var filename = Path . GetFileName ( new Uri ( url ) . LocalPath ) ;
httpClient ? ? = new ( ) ;
var response = await httpClient . GetAsync ( url ) ;
using var stream = await response . Content . ReadAsStreamAsync ( ) ;
mimeType = response . Content . Headers . ContentType ? . MediaType ;
if ( response . Content . Headers . ContentLength is long length )
return await UploadFileAsync ( new Helpers . IndirectStream ( stream ) { ContentLength = length } , filename ) ;
else
{
using var ms = new MemoryStream ( ) ;
await stream . CopyToAsync ( ms ) ;
ms . Position = 0 ;
return await UploadFileAsync ( ms , filename ) ;
}
}
}
}
var lastMedia = multiMedia [ ^ 1 ] ;
lastMedia . message = caption ;
lastMedia . entities = entities ;
if ( entities ! = null ) lastMedia . flags = InputSingleMedia . Flags . has_entities ;
var updates = await this . Messages_SendMultiMedia ( peer , multiMedia , reply_to_msg_id : reply_to_msg_id , schedule_date : schedule_date ) ;
2022-07-29 15:24:18 +02:00
RaiseUpdate ( updates ) ;
2022-10-12 23:25:15 +02:00
var msgIds = new int [ length ] ;
var result = new Message [ length ] ;
2022-04-06 18:38:54 +02:00
foreach ( var update in updates . UpdateList )
{
switch ( update )
{
2022-07-24 11:14:08 +02:00
case UpdateMessageID updMsgId : msgIds [ updMsgId . random_id - random_id ] = updMsgId . id ; break ;
case UpdateNewMessage { message : Message message } : result [ Array . IndexOf ( msgIds , message . id ) ] = message ; break ;
case UpdateNewScheduledMessage { message : Message schedMsg } : result [ Array . IndexOf ( msgIds , schedMsg . id ) ] = schedMsg ; break ;
2022-04-06 18:38:54 +02:00
}
}
2022-07-24 11:14:08 +02:00
return result ;
2022-04-06 18:38:54 +02: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 ,
} ;
/// <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>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>The file type of the photo</returns>
public async Task < Storage_FileType > DownloadFileAsync ( Photo photo , Stream outputStream , PhotoSizeBase photoSize = null , ProgressCallback progress = null )
{
if ( photoSize is PhotoStrippedSize psp )
return InflateStrippedThumb ( outputStream , psp . bytes ) ? Storage_FileType . jpeg : 0 ;
photoSize ? ? = photo . LargestPhotoSize ;
var fileLocation = photo . ToFileLocation ( photoSize ) ;
return await DownloadFileAsync ( fileLocation , outputStream , photo . dc_id , photoSize . FileSize , progress ) ;
}
/// <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>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>MIME type of the document/thumbnail</returns>
public async Task < string > DownloadFileAsync ( Document document , Stream outputStream , PhotoSizeBase thumbSize = null , ProgressCallback progress = null )
{
if ( thumbSize is PhotoStrippedSize psp )
return InflateStrippedThumb ( outputStream , psp . bytes ) ? "image/jpeg" : null ;
var fileLocation = document . ToFileLocation ( thumbSize ) ;
var fileType = await DownloadFileAsync ( fileLocation , outputStream , document . dc_id , thumbSize ? . FileSize ? ? document . size , progress ) ;
return thumbSize = = null ? document . mime_type : "image/" + fileType ;
}
/// <summary>Download a file from Telegram into the outputStream</summary>
/// <param name="fileLocation">Telegram file identifier, typically obtained with a .ToFileLocation() call</param>
/// <param name="outputStream">Stream to write file content to. This method does not close/dispose the stream</param>
/// <param name="dc_id">(optional) DC on which the file is stored</param>
/// <param name="fileSize">(optional) Expected file size</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>The file type</returns>
2022-06-15 01:58:44 +02:00
public async Task < Storage_FileType > DownloadFileAsync ( InputFileLocationBase fileLocation , Stream outputStream , int dc_id = 0 , long fileSize = 0 , ProgressCallback progress = null )
2022-04-06 18:38:54 +02:00
{
Storage_FileType fileType = Storage_FileType . unknown ;
var client = dc_id = = 0 ? this : await GetClientForDC ( dc_id , true ) ;
using var writeSem = new SemaphoreSlim ( 1 ) ;
2022-10-07 11:24:08 +02:00
bool canSeek = outputStream . CanSeek ;
2022-04-06 18:38:54 +02:00
long streamStartPos = outputStream . Position ;
2022-06-15 01:58:44 +02:00
long fileOffset = 0 , maxOffsetSeen = 0 ;
2022-04-06 18:38:54 +02:00
long transmitted = 0 ;
2022-06-15 01:58:44 +02:00
var tasks = new Dictionary < long , Task > ( ) ;
2022-04-06 18:38:54 +02:00
progress ? . Invoke ( 0 , fileSize ) ;
bool abort = false ;
while ( ! abort )
{
await _parallelTransfers . WaitAsync ( ) ;
var task = LoadPart ( fileOffset ) ;
lock ( tasks ) tasks [ fileOffset ] = task ;
if ( dc_id = = 0 ) { await task ; dc_id = client . _dcSession . DcID ; }
2022-10-07 11:24:08 +02:00
if ( ! canSeek ) await task ;
2022-04-06 18:38:54 +02:00
fileOffset + = FilePartSize ;
if ( fileSize ! = 0 & & fileOffset > = fileSize )
{
if ( await task ! = ( ( fileSize - 1 ) % FilePartSize ) + 1 )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Downloaded file size does not match expected file size" ) ;
2022-04-06 18:38:54 +02:00
break ;
}
2022-06-15 01:58:44 +02:00
async Task < int > LoadPart ( long offset )
2022-04-06 18:38:54 +02:00
{
Upload_FileBase fileBase ;
try
{
fileBase = await client . Upload_GetFile ( fileLocation , offset , FilePartSize ) ;
}
2022-04-11 12:08:17 +02:00
catch ( RpcException ex ) when ( ex . Code = = 303 & & ex . Message = = "FILE_MIGRATE_X" )
2022-04-06 18:38:54 +02:00
{
2022-04-11 12:08:17 +02:00
client = await GetClientForDC ( ex . X , true ) ;
2022-04-06 18:38:54 +02:00
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 )
2023-04-02 13:44:23 +02:00
throw new WTException ( "Upload_GetFile returned unsupported " + fileBase ? . GetType ( ) . Name ) ;
2022-04-06 18:38:54 +02:00
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 ) ;
transmitted + = fileData . bytes . Length ;
2023-04-25 13:19:15 +02:00
progress ? . Invoke ( transmitted , fileSize ) ;
2022-04-06 18:38:54 +02:00
}
catch ( Exception )
{
abort = true ;
throw ;
}
finally
{
writeSem . Release ( ) ;
}
}
lock ( tasks ) tasks . Remove ( offset ) ;
return fileData . bytes . Length ;
}
}
Task [ ] remainingTasks ;
lock ( tasks ) remainingTasks = tasks . Values . ToArray ( ) ;
await Task . WhenAll ( remainingTasks ) ; // wait completion and eventually propagate any task exception
await outputStream . FlushAsync ( ) ;
2022-10-07 11:24:08 +02:00
if ( canSeek ) outputStream . Seek ( streamStartPos + maxOffsetSeen , SeekOrigin . Begin ) ;
2022-04-06 18:38:54 +02:00
return fileType ;
}
/// <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>
/// <param name="miniThumb">Whether to extract the embedded very low-res thumbnail (synchronous, no actual download needed)</param>
/// <returns>The file type of the photo, or 0 if no photo available</returns>
public async Task < Storage_FileType > DownloadProfilePhotoAsync ( IPeerInfo peer , Stream outputStream , bool big = false , bool miniThumb = false )
{
int dc_id ;
long photo_id ;
byte [ ] stripped_thumb ;
switch ( peer )
{
case User user :
if ( user . photo = = null ) return 0 ;
dc_id = user . photo . dc_id ;
photo_id = user . photo . photo_id ;
stripped_thumb = user . photo . stripped_thumb ;
break ;
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 ;
break ;
default :
return 0 ;
}
if ( miniThumb & & ! big )
return InflateStrippedThumb ( outputStream , stripped_thumb ) ? Storage_FileType . jpeg : 0 ;
var fileLocation = new InputPeerPhotoFileLocation { peer = peer . ToInputPeer ( ) , photo_id = photo_id } ;
if ( big ) fileLocation . flags | = InputPeerPhotoFileLocation . Flags . big ;
return await DownloadFileAsync ( fileLocation , outputStream , dc_id ) ;
}
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 ;
}
2023-04-24 22:03:03 +02:00
/// <summary>Get all chats, channels and supergroups</summary>
public async Task < Messages_Chats > Messages_GetAllChats ( )
{
var dialogs = await Messages_GetAllDialogs ( ) ;
return new Messages_Chats { chats = dialogs . chats } ;
}
2022-04-06 18:38:54 +02:00
/// <summary>Returns the current user dialog list. <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.getDialogs#possible-errors">details</a>)</para></summary>
/// <param name="folder_id"><a href="https://corefork.telegram.org/api/folders#peer-folders">Peer folder ID, for more info click here</a></param>
/// <returns>See <a href="https://corefork.telegram.org/constructor/messages.dialogs"/></returns>
public async Task < Messages_Dialogs > Messages_GetAllDialogs ( int? folder_id = null )
{
var dialogs = await this . Messages_GetDialogs ( folder_id : folder_id ) ;
switch ( dialogs )
{
case Messages_DialogsSlice mds :
var dialogList = new List < DialogBase > ( ) ;
var messageList = new List < MessageBase > ( ) ;
while ( dialogs . Dialogs . Length ! = 0 )
{
dialogList . AddRange ( dialogs . Dialogs ) ;
messageList . AddRange ( dialogs . Messages ) ;
var lastDialog = dialogs . Dialogs [ ^ 1 ] ;
var lastMsg = dialogs . Messages . LastOrDefault ( m = > m . Peer . ID = = lastDialog . Peer . ID & & m . ID = = lastDialog . TopMessage ) ;
var offsetPeer = dialogs . UserOrChat ( lastDialog ) . ToInputPeer ( ) ;
dialogs = await this . Messages_GetDialogs ( lastMsg ? . Date ? ? default , lastDialog . TopMessage , offsetPeer , folder_id : folder_id ) ;
if ( dialogs is not Messages_Dialogs md ) break ;
foreach ( var ( key , value ) in md . chats ) mds . chats [ key ] = value ;
foreach ( var ( key , value ) in md . users ) mds . users [ key ] = value ;
}
mds . dialogs = dialogList . ToArray ( ) ;
mds . messages = messageList . ToArray ( ) ;
return mds ;
case Messages_Dialogs md : return md ;
2023-04-02 13:44:23 +02:00
default : throw new WTException ( "Messages_GetDialogs returned unexpected " + dialogs ? . GetType ( ) . Name ) ;
2022-04-06 18:38:54 +02: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>
/// <param name="includeKickBan">Also fetch the kicked/banned members?</param>
/// <param name="alphabet1">first letters used to search for in participants names<br/>(default values crafted with ♥ to find most latin and cyrillic names)</param>
/// <param name="alphabet2">second (and further) letters used to search for in participants names</param>
/// <param name="cancellationToken">Can be used to abort the work of this method</param>
/// <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 broadcast 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 , string alphabet1 = "А БCДЕ ЄЖФГHИІ JК ЛМ Н О ПQР С Т У В WХ ЦЧШЩЫЮЯЗ " , string alphabet2 = "А CЕ HИJЛМ Н О Р С Т У В WЫ" , CancellationToken cancellationToken = default )
{
alphabet2 ? ? = alphabet1 ;
var result = new Channels_ChannelParticipants { chats = new ( ) , users = new ( ) } ;
var user_ids = new HashSet < long > ( ) ;
var participants = new List < ChannelParticipantBase > ( ) ;
var mcf = await this . Channels_GetFullChannel ( channel ) ;
result . count = mcf . full_chat . ParticipantsCount ;
if ( result . count > 2000 & & ( ( Channel ) mcf . chats [ channel . ChannelId ] ) . IsChannel )
Helpers . Log ( 2 , "Fetching all participants on a big channel can take several minutes..." ) ;
await GetWithFilter ( new ChannelParticipantsAdmins ( ) ) ;
await GetWithFilter ( new ChannelParticipantsBots ( ) ) ;
await GetWithFilter ( new ChannelParticipantsSearch { q = "" } , ( f , c ) = > new ChannelParticipantsSearch { q = f . q + c } , alphabet1 ) ;
if ( includeKickBan )
{
await GetWithFilter ( new ChannelParticipantsKicked { q = "" } , ( f , c ) = > new ChannelParticipantsKicked { q = f . q + c } , alphabet1 ) ;
await GetWithFilter ( new ChannelParticipantsBanned { q = "" } , ( f , c ) = > new ChannelParticipantsBanned { q = f . q + c } , alphabet1 ) ;
}
result . participants = participants . ToArray ( ) ;
return result ;
async Task GetWithFilter < T > ( T filter , Func < T , char , T > recurse = null , string alphabet = null ) where T : ChannelParticipantsFilter
{
Channels_ChannelParticipants ccp ;
int maxCount = 0 ;
for ( int offset = 0 ; ; )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
ccp = await this . Channels_GetParticipants ( channel , filter , offset , 1024 , 0 ) ;
if ( ccp . count > maxCount ) maxCount = ccp . count ;
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 )
2023-04-08 16:32:19 +02:00
if ( user_ids . Add ( participant . UserId ) )
2022-04-06 18:38:54 +02:00
participants . Add ( participant ) ;
offset + = ccp . participants . Length ;
if ( offset > = ccp . count | | ccp . participants . Length = = 0 ) break ;
}
Helpers . Log ( 0 , $"GetParticipants({(filter as ChannelParticipantsSearch)?.q}) returned {ccp.count}/{maxCount}.\tAccumulated count: {participants.Count}" ) ;
if ( recurse ! = null & & ( ccp . count < maxCount - 100 | | ccp . count = = 200 | | ccp . count = = 1000 ) )
foreach ( var c in alphabet )
await GetWithFilter ( recurse ( filter , c ) , recurse , c = = 'А ' ? alphabet : alphabet2 ) ;
}
}
2022-12-19 14:14:17 +01:00
/// <summary>Helper simplified method: Get the admin log of a <a href="https://corefork.telegram.org/api/channel">channel/supergroup</a> <para>See <a href="https://corefork.telegram.org/method/channels.getAdminLog"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403 (<a href="https://corefork.telegram.org/method/channels.getAdminLog#possible-errors">details</a>)</para></summary>
/// <param name="channel">Channel</param>
/// <param name="q">Search query, can be empty</param>
/// <param name="events_filter">Event filter</param>
/// <param name="admin">Only show events from this admin</param>
public async Task < Channels_AdminLogResults > Channels_GetAdminLog ( InputChannelBase channel , ChannelAdminLogEventsFilter . Flags events_filter = 0 , string q = null , InputUserBase admin = null )
{
var admins = admin = = null ? null : new [ ] { admin } ;
var result = await this . Channels_GetAdminLog ( channel , q , events_filter : events_filter , admins : admins ) ;
if ( result . events . Length < 100 ) return result ;
var resultFull = result ;
List < ChannelAdminLogEvent > events = new ( result . events ) ;
do
{
result = await this . Channels_GetAdminLog ( channel , q , max_id : result . events [ ^ 1 ] . id , events_filter : events_filter , admins : admins ) ;
events . AddRange ( result . events ) ;
foreach ( var kvp in result . chats ) resultFull . chats [ kvp . Key ] = kvp . Value ;
foreach ( var kvp in result . users ) resultFull . users [ kvp . Key ] = kvp . Value ;
} while ( result . events . Length > = 100 ) ;
resultFull . events = events . ToArray ( ) ;
return resultFull ;
}
2022-09-24 15:30:43 +02:00
private const string OnlyChatChannel = "This method works on Chat & Channel only" ;
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Adds a single user to a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.addChatUser"/><br/> and <a href="https://corefork.telegram.org/method/channels.inviteToChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="user">User to be added</param>
public Task < UpdatesBase > AddChatUser ( InputPeer peer , InputUserBase user ) = > peer switch
2022-04-06 18:38:54 +02:00
{
2022-11-19 02:03:48 +01:00
InputPeerChat chat = > this . Messages_AddChatUser ( chat . chat_id , user , int . MaxValue ) ,
2022-12-02 01:42:39 +01:00
InputPeerChannel channel = > this . Channels_InviteToChannel ( channel , user ) ,
2022-09-24 15:30:43 +02:00
_ = > throw new ArgumentException ( OnlyChatChannel ) ,
2022-04-06 18:38:54 +02:00
} ;
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Kick a user from a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/channels.editBanned"/><br/> and <a href="https://corefork.telegram.org/method/messages.deleteChatUser"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="user">User to be removed</param>
2022-06-23 01:51:55 +02:00
public Task < UpdatesBase > DeleteChatUser ( InputPeer peer , InputUser user ) = > peer switch
2022-04-06 18:38:54 +02:00
{
2022-06-23 01:51:55 +02:00
InputPeerChat chat = > this . Messages_DeleteChatUser ( chat . chat_id , user , true ) ,
2022-04-06 18:38:54 +02:00
InputPeerChannel channel = > this . Channels_EditBanned ( channel , user , new ChatBannedRights { flags = ChatBannedRights . Flags . view_messages } ) ,
2022-09-24 15:30:43 +02:00
_ = > throw new ArgumentException ( OnlyChatChannel ) ,
2022-04-06 18:38:54 +02:00
} ;
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Leave a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.deleteChatUser"/><br/> and <a href="https://corefork.telegram.org/method/channels.leaveChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel to leave</param>
2022-06-23 01:51:55 +02:00
public Task < UpdatesBase > LeaveChat ( InputPeer peer ) = > peer switch
2022-04-06 18:38:54 +02:00
{
2022-06-23 01:51:55 +02:00
InputPeerChat chat = > this . Messages_DeleteChatUser ( chat . chat_id , InputUser . Self , true ) ,
2022-04-06 18:38:54 +02:00
InputPeerChannel channel = > this . Channels_LeaveChannel ( channel ) ,
2022-09-24 15:30:43 +02:00
_ = > throw new ArgumentException ( OnlyChatChannel ) ,
2022-04-06 18:38:54 +02:00
} ;
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Make a user admin in a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.editChatAdmin"/><br/> and <a href="https://corefork.telegram.org/method/channels.editAdmin"/> [bots: ✓]</para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="user">The user to make admin</param>
/// <param name="is_admin">Whether to make them admin</param>
2022-04-06 18:38:54 +02:00
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 > ( ) ,
2022-12-02 01:42:39 +01:00
chats = ( await this . Messages_GetChats ( chat . chat_id ) ) . chats } ;
2022-04-06 18:38:54 +02:00
case InputPeerChannel channel :
return await this . Channels_EditAdmin ( channel , user ,
new ChatAdminRights { flags = is_admin ? ( ChatAdminRights . Flags ) 0x8BF : 0 } , null ) ;
default :
2022-09-24 15:30:43 +02:00
throw new ArgumentException ( OnlyChatChannel ) ;
2022-04-06 18:38:54 +02:00
}
}
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Change the photo of a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.editChatPhoto"/><br/> and <a href="https://corefork.telegram.org/method/channels.editPhoto"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="photo">New photo</param>
2022-04-06 18:38:54 +02:00
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 ) ,
2022-09-24 15:30:43 +02:00
_ = > throw new ArgumentException ( OnlyChatChannel ) ,
2022-04-06 18:38:54 +02:00
} ;
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Edit the name of a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.editChatTitle"/><br/> and <a href="https://corefork.telegram.org/method/channels.editTitle"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="title">New name</param>
2022-04-06 18:38:54 +02:00
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 ) ,
2022-09-24 15:30:43 +02:00
_ = > throw new ArgumentException ( OnlyChatChannel ) ,
2022-04-06 18:38:54 +02:00
} ;
2022-11-19 02:03:48 +01:00
/// <summary>Get full info about a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.getFullChat"/><br/> and <a href="https://corefork.telegram.org/method/channels.getFullChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
/// <param name="peer">Chat/Channel</param>
2022-04-06 18:38:54 +02:00
public Task < Messages_ChatFull > GetFullChat ( InputPeer peer ) = > peer switch
{
InputPeerChat chat = > this . Messages_GetFullChat ( chat . chat_id ) ,
InputPeerChannel channel = > this . Channels_GetFullChannel ( channel ) ,
2022-09-24 15:30:43 +02:00
_ = > throw new ArgumentException ( OnlyChatChannel ) ,
2022-04-06 18:38:54 +02:00
} ;
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Delete a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.deleteChat"/><br/> and <a href="https://corefork.telegram.org/method/channels.deleteChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
/// <param name="peer">Chat/Channel to delete</param>
2022-04-06 18:38:54 +02: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 > ( ) ,
2022-12-02 01:42:39 +01:00
chats = ( await this . Messages_GetChats ( chat . chat_id ) ) . chats } ;
2022-04-06 18:38:54 +02:00
case InputPeerChannel channel :
return await this . Channels_DeleteChannel ( channel ) ;
default :
2022-09-24 15:30:43 +02:00
throw new ArgumentException ( OnlyChatChannel ) ;
2022-04-06 18:38:54 +02:00
}
}
2022-05-20 14:54:07 +02:00
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Get individual messages by IDs [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.getMessages"/><br/> and <a href="https://corefork.telegram.org/method/channels.getMessages"/></para> <para>Possible <see cref="RpcException"/> codes: 400</para></summary>
/// <param name="peer">User/Chat/Channel</param>
/// <param name="id">IDs of messages to get</param>
2022-09-24 15:30:43 +02:00
public Task < Messages_MessagesBase > GetMessages ( InputPeer peer , params InputMessage [ ] id )
= > peer is InputPeerChannel channel ? this . Channels_GetMessages ( channel , id ) : this . Messages_GetMessages ( id ) ;
2022-05-21 02:32:15 +02:00
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Delete messages by IDs [bots: ✓]<br/>Messages are deleted for all participants <para>See <a href="https://corefork.telegram.org/method/messages.deleteMessages"/><br/> and <a href="https://corefork.telegram.org/method/channels.deleteMessages"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">User/Chat/Channel</param>
/// <param name="id">IDs of messages to delete</param>
2022-09-24 15:30:43 +02:00
public Task < Messages_AffectedMessages > DeleteMessages ( InputPeer peer , params int [ ] id )
2022-11-19 02:03:48 +01:00
= > peer is InputPeerChannel channel ? this . Channels_DeleteMessages ( channel , id ) : this . Messages_DeleteMessages ( id , true ) ;
2022-10-25 10:34:53 +02:00
2022-11-19 02:03:48 +01:00
/// <summary>Generic helper: Marks message history as read. <para>See <a href="https://corefork.telegram.org/method/messages.readHistory"/><br/> and <a href="https://corefork.telegram.org/method/channels.readHistory"/></para> <para>Possible <see cref="RpcException"/> codes: 400</para></summary>
/// <param name="peer">User/Chat/Channel</param>
2022-10-25 10:34:53 +02:00
/// <param name="max_id">If a positive value is passed, only messages with identifiers less or equal than the given one will be marked read</param>
2022-11-19 02:03:48 +01:00
public async Task < bool > ReadHistory ( InputPeer peer , int max_id = default )
= > peer is InputPeerChannel channel ? await this . Channels_ReadHistory ( channel , max_id ) : ( await this . Messages_ReadHistory ( peer , max_id ) ) ! = null ;
2023-02-17 19:31:01 +01:00
private static readonly char [ ] QueryOrFragment = new [ ] { '?' , '#' } ;
/// <summary>Return information about a chat/channel based on Invite Link</summary>
/// <param name="url">Channel or Invite Link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname</param>
/// <param name="join"><see langword="true"/> to also join the chat/channel</param>
/// <returns>a Chat or Channel, possibly partial Channel information only (with flag <see cref="Channel.Flags.min"/>)</returns>
public async Task < ChatBase > AnalyzeInviteLink ( string url , bool join = false )
{
int start = url . IndexOf ( "//" ) ;
start = url . IndexOf ( '/' , start + 2 ) + 1 ;
int end = url . IndexOfAny ( QueryOrFragment , start ) ;
if ( end = = - 1 ) end = url . Length ;
if ( start = = 0 | | end = = start ) throw new ArgumentException ( "Invalid URI" ) ;
string hash ;
if ( url [ start ] = = '+' )
hash = url [ ( start + 1 ) . . end ] ;
else if ( string . Compare ( url , start , "joinchat/" , 0 , 9 , StringComparison . OrdinalIgnoreCase ) = = 0 )
hash = url [ ( start + 9 ) . . end ] ;
else
{
var resolved = await this . Contacts_ResolveUsername ( url [ start . . end ] ) ;
var chat = resolved . Chat ;
if ( join & & chat ! = null )
{
var res = await this . Channels_JoinChannel ( ( Channel ) chat ) ;
chat = res . Chats [ chat . ID ] ;
}
return chat ;
}
var chatInvite = await this . Messages_CheckChatInvite ( hash ) ;
if ( join )
try
{
var res = await this . Messages_ImportChatInvite ( hash ) ;
if ( res . Chats . Values . FirstOrDefault ( ) is ChatBase chat ) return chat ;
}
catch ( RpcException ex ) when ( ex . Code = = 400 & & ex . Message = = "INVITE_REQUEST_SENT" ) { }
switch ( chatInvite )
{
case ChatInviteAlready cia : return cia . chat ;
case ChatInvitePeek cip : return cip . chat ;
case ChatInvite ci :
ChatPhoto chatPhoto = null ;
if ( ci . photo is Photo photo )
{
var stripped_thumb = photo . sizes . OfType < PhotoStrippedSize > ( ) . FirstOrDefault ( ) ? . bytes ;
chatPhoto = new ChatPhoto
{
dc_id = photo . dc_id ,
photo_id = photo . id ,
stripped_thumb = stripped_thumb ,
flags = ( stripped_thumb ! = null ? ChatPhoto . Flags . has_stripped_thumb : 0 ) |
( photo . flags . HasFlag ( Photo . Flags . has_video_sizes ) ? ChatPhoto . Flags . has_video : 0 ) ,
} ;
}
var rrAbout = ci . about = = null ? null : new RestrictionReason [ ] { new ( ) { text = ci . about } } ;
return ! ci . flags . HasFlag ( ChatInvite . Flags . channel )
? new Chat { title = ci . title , photo = chatPhoto , participants_count = ci . participants_count }
: new Channel { title = ci . title , photo = chatPhoto , participants_count = ci . participants_count ,
restriction_reason = rrAbout ,
flags = ( ci . flags . HasFlag ( ChatInvite . Flags . broadcast ) ? Channel . Flags . broadcast | Channel . Flags . min : Channel . Flags . min ) |
( ci . flags . HasFlag ( ChatInvite . Flags . public_ ) ? Channel . Flags . has_username : 0 ) |
( ci . flags . HasFlag ( ChatInvite . Flags . megagroup ) ? Channel . Flags . megagroup : 0 ) |
( ci . flags . HasFlag ( ChatInvite . Flags . request_needed ) ? Channel . Flags . join_request : 0 ) } ;
}
return null ;
}
/// <summary>Return chat and message details based on a message URL</summary>
/// <param name="url">Message Link, like https://t.me/c/1234567890/1234 or https://t.me/channelname/1234</param>
/// <returns>Structure containing the message, chat and user details</returns>
/// <remarks>If link is for private group (<c>t.me/c/..</c>), user must have joined the group</remarks>
public async Task < Messages_ChannelMessages > GetMessageByLink ( string url )
{
int start = url . IndexOf ( "//" ) ;
start = url . IndexOf ( '/' , start + 2 ) + 1 ;
int slash = url . IndexOf ( '/' , start + 2 ) ;
if ( start = = 0 | | slash = = - 1 ) throw new ArgumentException ( "Invalid URI" ) ;
int end = url . IndexOfAny ( QueryOrFragment , slash + 1 ) ;
if ( end = = - 1 ) end = url . Length ;
ChatBase chat ;
if ( url [ start ] is 'c' or 'C' & & url [ start + 1 ] = = '/' )
{
long chatId = long . Parse ( url [ ( start + 2 ) . . slash ] ) ;
var chats = await this . Channels_GetChannels ( new InputChannel ( chatId , 0 ) ) ;
if ( ! chats . chats . TryGetValue ( chatId , out chat ) )
2023-04-02 13:44:23 +02:00
throw new WTException ( $"Channel {chatId} not found" ) ;
2023-02-17 19:31:01 +01:00
}
else
{
var resolved = await this . Contacts_ResolveUsername ( url [ start . . slash ] ) ;
chat = resolved . Chat ;
2023-04-02 13:44:23 +02:00
if ( chat is null ) throw new WTException ( $"@{url[start..slash]} is not a Chat/Channel" ) ;
2023-02-17 19:31:01 +01:00
}
int msgId = int . Parse ( url [ ( slash + 1 ) . . end ] ) ;
return await this . Channels_GetMessages ( ( Channel ) chat , msgId ) as Messages_ChannelMessages ;
}
2022-04-06 18:38:54 +02:00
#endregion
}
}