2022-03-23 17:04:17 +01:00
using System ;
using System.Collections.Generic ;
2022-03-21 21:25:30 +01:00
using System.Linq ;
using System.Text ;
using System.Web ;
namespace TL
{
public static class Extensions
{
private class CollectorPeer : Peer
{
public override long ID = > 0 ;
internal Dictionary < long , User > _users ;
internal Dictionary < long , ChatBase > _chats ;
internal override IPeerInfo UserOrChat ( Dictionary < long , User > users , Dictionary < long , ChatBase > chats )
{
lock ( _users )
foreach ( var user in users . Values )
if ( user ! = null )
if ( ! user . flags . HasFlag ( User . Flags . min ) | | ! _users . TryGetValue ( user . id , out var prevUser ) | | prevUser . flags . HasFlag ( User . Flags . min ) )
_users [ user . id ] = user ;
lock ( _chats )
foreach ( var kvp in chats )
if ( kvp . Value is not Channel channel )
_chats [ kvp . Key ] = kvp . Value ;
else if ( ! channel . flags . HasFlag ( Channel . Flags . min ) | | ! _chats . TryGetValue ( channel . id , out var prevChat ) | | prevChat is not Channel prevChannel | | prevChannel . flags . HasFlag ( Channel . Flags . min ) )
_chats [ kvp . Key ] = channel ;
return null ;
}
}
/// <summary>Accumulate users/chats found in this structure in your dictionaries, ignoring <see href="https://core.telegram.org/api/min">Min constructors</see> when the full object is already stored</summary>
/// <param name="structure">The structure having a <c>users</c></param>
/// <param name="users"></param>
/// <param name="chats"></param>
public static void CollectUsersChats ( this IPeerResolver structure , Dictionary < long , User > users , Dictionary < long , ChatBase > chats )
= > structure . UserOrChat ( new CollectorPeer { _users = users , _chats = chats } ) ;
}
public static class Markdown
{
2022-03-23 17:33:23 +01:00
/// <summary>Converts a <a href="https://core.telegram.org/bots/api/#markdownv2-style">Markdown text</a> into the (plain text + entities) format used by Telegram messages</summary>
2022-03-21 21:25:30 +01:00
/// <param name="client">Client, used for getting access_hash for <c>tg://user?id=</c> URLs</param>
/// <param name="text">[in] The Markdown text<br/>[out] The same (plain) text, stripped of all Markdown notation</param>
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="WTelegram.Client.SendMessageAsync">SendMessageAsync</see> or <see cref="WTelegram.Client.SendMediaAsync">SendMediaAsync</see></returns>
public static MessageEntity [ ] MarkdownToEntities ( this WTelegram . Client client , ref string text )
{
var entities = new List < MessageEntity > ( ) ;
var sb = new StringBuilder ( text ) ;
for ( int offset = 0 ; offset < sb . Length ; )
{
switch ( sb [ offset ] )
{
2022-03-23 17:04:17 +01:00
case '\r' : sb . Remove ( offset , 1 ) ; break ;
2022-03-21 21:25:30 +01:00
case '\\' : sb . Remove ( offset + + , 1 ) ; break ;
case '*' : ProcessEntity < MessageEntityBold > ( ) ; break ;
case '~' : ProcessEntity < MessageEntityStrike > ( ) ; break ;
case '_' :
if ( offset + 1 < sb . Length & & sb [ offset + 1 ] = = '_' )
{
sb . Remove ( offset , 1 ) ;
ProcessEntity < MessageEntityUnderline > ( ) ;
}
else
ProcessEntity < MessageEntityItalic > ( ) ;
break ;
case '|' :
if ( offset + 1 < sb . Length & & sb [ offset + 1 ] = = '|' )
{
sb . Remove ( offset , 1 ) ;
ProcessEntity < MessageEntitySpoiler > ( ) ;
}
else
offset + + ;
break ;
case '`' :
if ( offset + 2 < sb . Length & & sb [ offset + 1 ] = = '`' & & sb [ offset + 2 ] = = '`' )
{
int len = 3 ;
if ( entities . FindLast ( e = > e . length = = - 1 ) is MessageEntityPre pre )
pre . length = offset - pre . offset ;
else
{
while ( offset + len < sb . Length & & ! char . IsWhiteSpace ( sb [ offset + len ] ) )
len + + ;
entities . Add ( new MessageEntityPre { offset = offset , length = - 1 , language = sb . ToString ( offset + 3 , len - 3 ) } ) ;
2022-03-23 17:04:17 +01:00
if ( sb [ offset + len ] = = '\n' ) len + + ;
2022-03-21 21:25:30 +01:00
}
sb . Remove ( offset , len ) ;
}
else
ProcessEntity < MessageEntityCode > ( ) ;
break ;
case '[' :
entities . Add ( new MessageEntityTextUrl { offset = offset , length = - 1 } ) ;
sb . Remove ( offset , 1 ) ;
break ;
case ']' :
if ( offset + 2 < sb . Length & & sb [ offset + 1 ] = = '(' )
{
var lastIndex = entities . FindLastIndex ( e = > e . length = = - 1 ) ;
if ( lastIndex > = 0 & & entities [ lastIndex ] is MessageEntityTextUrl textUrl )
{
textUrl . length = offset - textUrl . offset ;
int offset2 = offset + 2 ;
while ( offset2 < sb . Length )
{
char c = sb [ offset2 + + ] ;
if ( c = = '\\' ) sb . Remove ( offset2 - 1 , 1 ) ;
else if ( c = = ')' ) break ;
}
textUrl . url = sb . ToString ( offset + 2 , offset2 - offset - 3 ) ;
if ( textUrl . url . StartsWith ( "tg://user?id=" ) & & long . TryParse ( textUrl . url [ 13. . ] , out var user_id ) & & client . GetAccessHashFor < User > ( user_id ) is long hash )
entities [ lastIndex ] = new InputMessageEntityMentionName { offset = textUrl . offset , length = textUrl . length , user_id = new InputUser { user_id = user_id , access_hash = hash } } ;
sb . Remove ( offset , offset2 - offset ) ;
break ;
}
}
offset + + ;
break ;
default : offset + + ; break ;
}
void ProcessEntity < T > ( ) where T : MessageEntity , new ( )
{
if ( entities . LastOrDefault ( e = > e . length = = - 1 ) is T prevEntity )
prevEntity . length = offset - prevEntity . offset ;
else
entities . Add ( new T { offset = offset , length = - 1 } ) ;
sb . Remove ( offset , 1 ) ;
}
}
text = sb . ToString ( ) ;
return entities . Count = = 0 ? null : entities . ToArray ( ) ;
}
2022-03-23 17:33:23 +01:00
/// <summary>Converts the (plain text + entities) format used by Telegram messages into a <a href="https://core.telegram.org/bots/api/#markdownv2-style">Markdown text</a></summary>
/// <param name="client">Client, used only for getting current user ID in case of <c>InputMessageEntityMentionName+InputUserSelf</c></param>
/// <param name="message">The plain text, typically obtained from <see cref="TL.Message.message"/></param>
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="TL.Message.entities"/></param>
/// <returns>The message text with MarkdownV2 formattings</returns>
2022-03-23 17:04:17 +01:00
public static string EntitiesToMarkdown ( this WTelegram . Client client , string message , MessageEntity [ ] entities )
{
if ( entities = = null | | entities . Length = = 0 ) return Escape ( message ) ;
var closings = new List < ( int offset , string md ) > ( ) ;
var sb = new StringBuilder ( message ) ;
int entityIndex = 0 ;
var nextEntity = entities [ entityIndex ] ;
for ( int offset = 0 , i = 0 ; ; offset + + , i + + )
{
while ( closings . Count ! = 0 & & offset = = closings [ 0 ] . offset )
{
var md = closings [ 0 ] . md ;
if ( i > 0 & & md [ 0 ] = = '_' & & sb [ i - 1 ] = = '_' ) md = '\r' + md ;
sb . Insert ( i , md ) ; i + = md . Length ;
closings . RemoveAt ( 0 ) ;
}
if ( i = = sb . Length ) break ;
while ( offset = = nextEntity ? . offset )
{
if ( entityToMD . TryGetValue ( nextEntity . GetType ( ) , out var md ) )
{
var closing = ( nextEntity . offset + nextEntity . length , md ) ;
if ( md [ 0 ] = = '[' )
{
if ( nextEntity is MessageEntityTextUrl metu )
closing . md = $"]({metu.url.Replace(" \ \ ", " \ \ \ \ ").Replace(" ) ", " \ \ ) ").Replace(" > ", " % 3 E ")})" ;
else if ( nextEntity is MessageEntityMentionName memn )
closing . md = $"](tg://user?id={memn.user_id})" ;
else if ( nextEntity is InputMessageEntityMentionName imemn )
closing . md = $"](tg://user?id={imemn.user_id.UserId ?? client.UserId})" ;
}
else if ( nextEntity is MessageEntityPre mep )
md = $"```{mep.language}\n" ;
int index = ~ closings . BinarySearch ( closing , Comparer < ( int , string ) > . Create ( ( x , y ) = > x . Item1 . CompareTo ( y . Item1 ) | 1 ) ) ;
closings . Insert ( index , closing ) ;
if ( i > 0 & & md [ 0 ] = = '_' & & sb [ i - 1 ] = = '_' ) md = '\r' + md ;
sb . Insert ( i , md ) ; i + = md . Length ;
}
nextEntity = + + entityIndex < entities . Length ? entities [ entityIndex ] : null ;
}
switch ( sb [ i ] )
{
case '_' : case '*' : case '~' : case '`' : case '#' : case '+' : case '-' : case '=' : case '.' : case '!' :
case '[' : case ']' : case '(' : case ')' : case '{' : case '}' : case '>' : case '|' : case '\\' :
sb . Insert ( i , '\\' ) ; i + + ;
break ;
}
}
return sb . ToString ( ) ;
}
static readonly Dictionary < Type , string > entityToMD = new ( )
{
[typeof(MessageEntityBold)] = "*" ,
[typeof(MessageEntityItalic)] = "_" ,
[typeof(MessageEntityCode)] = "`" ,
[typeof(MessageEntityPre)] = "```" ,
[typeof(MessageEntityTextUrl)] = "[" ,
[typeof(MessageEntityMentionName)] = "[" ,
[typeof(InputMessageEntityMentionName)] = "[" ,
[typeof(MessageEntityUnderline)] = "__" ,
[typeof(MessageEntityStrike)] = "~" ,
[typeof(MessageEntitySpoiler)] = "||" ,
} ;
2022-03-21 21:25:30 +01:00
/// <summary>Insert backslashes in front of Markdown reserved characters</summary>
/// <param name="text">The text to escape</param>
/// <returns>The escaped text, ready to be used in <see cref="MarkdownToEntities">MarkdownToEntities</see> without problems</returns>
public static string Escape ( string text )
{
StringBuilder sb = null ;
for ( int index = 0 , added = 0 ; index < text . Length ; index + + )
{
switch ( text [ index ] )
{
case '_' : case '*' : case '~' : case '`' : case '#' : case '+' : case '-' : case '=' : case '.' : case '!' :
case '[' : case ']' : case '(' : case ')' : case '{' : case '}' : case '>' : case '|' : case '\\' :
sb ? ? = new StringBuilder ( text , text . Length + 32 ) ;
sb . Insert ( index + added + + , '\\' ) ;
break ;
}
}
return sb ? . ToString ( ) ? ? text ;
}
}
public static class HtmlText
{
2022-03-23 17:33:23 +01:00
/// <summary>Converts an <a href="https://core.telegram.org/bots/api/#html-style">HTML-formatted text</a> into the (plain text + entities) format used by Telegram messages</summary>
2022-03-21 21:25:30 +01:00
/// <param name="client">Client, used for getting access_hash for <c>tg://user?id=</c> URLs</param>
/// <param name="text">[in] The HTML-formatted text<br/>[out] The same (plain) text, stripped of all HTML tags</param>
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="WTelegram.Client.SendMessageAsync">SendMessageAsync</see> or <see cref="WTelegram.Client.SendMediaAsync">SendMediaAsync</see></returns>
public static MessageEntity [ ] HtmlToEntities ( this WTelegram . Client client , ref string text )
{
var entities = new List < MessageEntity > ( ) ;
var sb = new StringBuilder ( text ) ;
int end ;
for ( int offset = 0 ; offset < sb . Length ; )
{
char c = sb [ offset ] ;
if ( c = = '&' )
{
for ( end = offset + 1 ; end < sb . Length ; end + + )
if ( sb [ end ] = = ';' ) break ;
if ( end > = sb . Length ) break ;
var html = HttpUtility . HtmlDecode ( sb . ToString ( offset , end - offset + 1 ) ) ;
if ( html . Length = = 1 )
{
sb [ offset ] = html [ 0 ] ;
sb . Remove ( + + offset , end - offset + 1 ) ;
}
else
offset = end + 1 ;
}
else if ( c = = '<' )
{
for ( end = + + offset ; end < sb . Length ; end + + )
if ( sb [ end ] = = '>' ) break ;
if ( end > = sb . Length ) break ;
bool closing = sb [ offset ] = = '/' ;
var tag = closing ? sb . ToString ( offset + 1 , end - offset - 1 ) : sb . ToString ( offset , end - offset ) ;
sb . Remove ( - - offset , end + 1 - offset ) ;
switch ( tag )
{
case "b" : case "strong" : ProcessEntity < MessageEntityBold > ( ) ; break ;
case "i" : case "em" : ProcessEntity < MessageEntityItalic > ( ) ; break ;
case "u" : case "ins" : ProcessEntity < MessageEntityUnderline > ( ) ; break ;
case "s" : case "strike" : case "del" : ProcessEntity < MessageEntityStrike > ( ) ; break ;
case "span class=\"tg-spoiler\"" :
case "span" when closing :
case "tg-spoiler" : ProcessEntity < MessageEntitySpoiler > ( ) ; break ;
case "code" : ProcessEntity < MessageEntityCode > ( ) ; break ;
case "pre" : ProcessEntity < MessageEntityPre > ( ) ; break ;
default :
if ( closing )
{
if ( tag = = "a" )
{
var prevEntity = entities . LastOrDefault ( e = > e . length = = - 1 ) ;
if ( prevEntity is InputMessageEntityMentionName or MessageEntityTextUrl )
prevEntity . length = offset - prevEntity . offset ;
}
}
else if ( tag . StartsWith ( "a href=\"" ) & & tag . EndsWith ( "\"" ) )
{
tag = tag [ 8. . ^ 1 ] ;
if ( tag . StartsWith ( "tg://user?id=" ) & & long . TryParse ( tag [ 13. . ] , out var user_id ) & & client . GetAccessHashFor < User > ( user_id ) is long hash )
entities . Add ( new InputMessageEntityMentionName { offset = offset , length = - 1 , user_id = new InputUser { user_id = user_id , access_hash = hash } } ) ;
else
entities . Add ( new MessageEntityTextUrl { offset = offset , length = - 1 , url = tag } ) ;
}
else if ( tag . StartsWith ( "code class=\"language-" ) & & tag . EndsWith ( "\"" ) )
{
if ( entities . LastOrDefault ( e = > e . length = = - 1 ) is MessageEntityPre prevEntity )
prevEntity . language = tag [ 21. . ^ 1 ] ;
}
break ;
}
void ProcessEntity < T > ( ) where T : MessageEntity , new ( )
{
if ( ! closing )
entities . Add ( new T { offset = offset , length = - 1 } ) ;
else if ( entities . LastOrDefault ( e = > e . length = = - 1 ) is T prevEntity )
prevEntity . length = offset - prevEntity . offset ;
}
}
else
offset + + ;
}
text = sb . ToString ( ) ;
return entities . Count = = 0 ? null : entities . ToArray ( ) ;
}
2022-03-23 17:33:23 +01:00
/// <summary>Converts the (plain text + entities) format used by Telegram messages into an <a href="https://core.telegram.org/bots/api/#html-style">HTML-formatted text</a></summary>
/// <param name="client">Client, used only for getting current user ID in case of <c>InputMessageEntityMentionName+InputUserSelf</c></param>
/// <param name="message">The plain text, typically obtained from <see cref="TL.Message.message"/></param>
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="TL.Message.entities"/></param>
/// <returns>The message text with HTML formatting tags</returns>
2022-03-23 17:04:17 +01:00
public static string EntitiesToHtml ( this WTelegram . Client client , string message , MessageEntity [ ] entities )
{
if ( entities = = null | | entities . Length = = 0 ) return Escape ( message ) ;
var closings = new List < ( int offset , string tag ) > ( ) ;
var sb = new StringBuilder ( message ) ;
int entityIndex = 0 ;
var nextEntity = entities [ entityIndex ] ;
for ( int offset = 0 , i = 0 ; ; offset + + , i + + )
{
while ( closings . Count ! = 0 & & offset = = closings [ 0 ] . offset )
{
var tag = closings [ 0 ] . tag ;
sb . Insert ( i , tag ) ; i + = tag . Length ;
closings . RemoveAt ( 0 ) ;
}
if ( i = = sb . Length ) break ;
while ( offset = = nextEntity ? . offset )
{
if ( entityToTag . TryGetValue ( nextEntity . GetType ( ) , out var tag ) )
{
var closing = ( nextEntity . offset + nextEntity . length , $"</{tag}>" ) ;
if ( tag [ 0 ] = = 'a' )
{
if ( nextEntity is MessageEntityTextUrl metu )
tag = $"<a href=\" { metu . url } \ ">" ;
else if ( nextEntity is MessageEntityMentionName memn )
tag = $"<a href=\" tg : //user?id={memn.user_id}\">";
else if ( nextEntity is InputMessageEntityMentionName imemn )
tag = $"<a href=\" tg : //user?id={imemn.user_id.UserId ?? client.UserId}\">";
}
else if ( nextEntity is MessageEntityPre mep & & ! string . IsNullOrEmpty ( mep . language ) )
{
closing . Item2 = "</code></pre>" ;
tag = $"<pre><code class=\" language - { mep . language } \ ">" ;
}
else
tag = $"<{tag}>" ;
int index = ~ closings . BinarySearch ( closing , Comparer < ( int , string ) > . Create ( ( x , y ) = > x . Item1 . CompareTo ( y . Item1 ) | 1 ) ) ;
closings . Insert ( index , closing ) ;
sb . Insert ( i , tag ) ; i + = tag . Length ;
}
nextEntity = + + entityIndex < entities . Length ? entities [ entityIndex ] : null ;
}
switch ( sb [ i ] )
{
case '&' : sb . Insert ( i + 1 , "amp;" ) ; i + = 4 ; break ;
case '<' : sb . Remove ( i , 1 ) . Insert ( i , "<" ) ; i + = 3 ; break ;
case '>' : sb . Remove ( i , 1 ) . Insert ( i , ">" ) ; i + = 3 ; break ;
}
}
return sb . ToString ( ) ;
}
static readonly Dictionary < Type , string > entityToTag = new ( )
{
[typeof(MessageEntityBold)] = "b" ,
[typeof(MessageEntityItalic)] = "i" ,
[typeof(MessageEntityCode)] = "code" ,
[typeof(MessageEntityPre)] = "pre" ,
[typeof(MessageEntityTextUrl)] = "a" ,
[typeof(MessageEntityMentionName)] = "a" ,
[typeof(InputMessageEntityMentionName)] = "a" ,
[typeof(MessageEntityUnderline)] = "u" ,
[typeof(MessageEntityStrike)] = "s" ,
[typeof(MessageEntitySpoiler)] = "tg-spoiler" ,
} ;
2022-03-21 21:25:30 +01:00
/// <summary>Replace special HTML characters with their &xx; equivalent</summary>
/// <param name="text">The text to make HTML-safe</param>
/// <returns>The HTML-safe text, ready to be used in <see cref="HtmlToEntities">HtmlToEntities</see> without problems</returns>
public static string Escape ( string text )
= > text . Replace ( "&" , "&" ) . Replace ( "<" , "<" ) . Replace ( ">" , ">" ) ;
}
}