diff --git a/EXAMPLES.md b/EXAMPLES.md
index 98800e5..176cb83 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -37,16 +37,25 @@ if (contacts.imported.Length > 0)
*Note: To prevent spam, Telegram may restrict your ability to add new phone numbers.*
-### Send a Markdown message to ourself (Saved Messages)
+
+### Send a Markdown or HTML-formatted message to ourself (Saved Messages)
```csharp
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
var user = await client.LoginUserIfNeeded();
+
+// Markdown-style text:
var text = $"Hello __dear *{Markdown.Escape(user.first_name)}*__\n" +
"Enjoy this `userbot` written with [WTelegramClient](https://github.com/wiz0u/WTelegramClient)";
var entities = client.MarkdownToEntities(ref text);
await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
+
+// HTML-formatted text:
+var text2 = $"Hello dear {HtmlText.Escape(user.first_name)}\n" +
+ "Enjoy this userbot written with WTelegramClient";
+var entities2 = client.HtmlToEntities(ref text2);
+await client.SendMessageAsync(InputPeer.Self, text2, entities: entities2);
```
-See [MarkdownV2 formatting style](https://core.telegram.org/bots/api/#markdownv2-style) for details.
+See [MarkdownV2 formatting style](https://core.telegram.org/bots/api/#markdownv2-style) and [HTML formatting style](https://core.telegram.org/bots/api/#html-style) for details.
*Note: For the `tg://user?id=` notation to work, that user's access hash must have been collected first ([see below](#collect-access-hash))*
diff --git a/src/TL.Helpers.cs b/src/TL.Helpers.cs
index 10ac32a..d446e76 100644
--- a/src/TL.Helpers.cs
+++ b/src/TL.Helpers.cs
@@ -488,7 +488,7 @@ namespace TL
public static class Markdown
{
- /// Converts a Markdown text into the (Entities + plain text) format used by Telegram messages
+ /// Converts a Markdown text into the (Entities + plain text) format used by Telegram messages
/// Client, used for getting access_hash for tg://user?id= URLs
/// [in] The Markdown text
[out] The same (plain) text, stripped of all Markdown notation
/// The array of formatting entities that you can pass (along with the plain text) to SendMessageAsync or SendMediaAsync
@@ -601,4 +601,99 @@ namespace TL
return sb?.ToString() ?? text;
}
}
+
+ public static class HtmlText
+ {
+ /// Converts an HTML-formatted text into the (Entities + plain text) format used by Telegram messages
+ /// Client, used for getting access_hash for tg://user?id= URLs
+ /// [in] The HTML-formatted text
[out] The same (plain) text, stripped of all HTML tags
+ /// The array of formatting entities that you can pass (along with the plain text) to SendMessageAsync or SendMediaAsync
+ public static MessageEntity[] HtmlToEntities(this WTelegram.Client client, ref string text)
+ {
+ var entities = new List();
+ 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(); break;
+ case "i": case "em": ProcessEntity(); break;
+ case "u": case "ins": ProcessEntity(); break;
+ case "s": case "strike": case "del": ProcessEntity(); break;
+ case "span class=\"tg-spoiler\"":
+ case "span" when closing:
+ case "tg-spoiler": ProcessEntity(); break;
+ case "code": ProcessEntity(); break;
+ case "pre": ProcessEntity(); 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_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() 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();
+ }
+
+ /// Replace special HTML characters with their &xx; equivalent
+ /// The text to make HTML-safe
+ /// The HTML-safe text, ready to be used in HtmlToEntities without problems
+ public static string Escape(string text)
+ => text.Replace("&", "&").Replace("<", "<").Replace(">", ">");
+ }
}