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(">", ">"); + } }