using System.Collections.Generic; 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 _users; internal Dictionary _chats; internal override IPeerInfo UserOrChat(Dictionary users, Dictionary 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; } } /// Accumulate users/chats found in this structure in your dictionaries, ignoring Min constructors when the full object is already stored /// The structure having a users /// /// public static void CollectUsersChats(this IPeerResolver structure, Dictionary users, Dictionary chats) => structure.UserOrChat(new CollectorPeer { _users = users, _chats = chats }); } public static class Markdown { /// 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 public static MessageEntity[] MarkdownToEntities(this WTelegram.Client client, ref string text) { var entities = new List(); var sb = new StringBuilder(text); for (int offset = 0; offset < sb.Length;) { switch (sb[offset]) { case '\\': sb.Remove(offset++, 1); break; case '*': ProcessEntity(); break; case '~': ProcessEntity(); break; case '_': if (offset + 1 < sb.Length && sb[offset + 1] == '_') { sb.Remove(offset, 1); ProcessEntity(); } else ProcessEntity(); break; case '|': if (offset + 1 < sb.Length && sb[offset + 1] == '|') { sb.Remove(offset, 1); ProcessEntity(); } 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) }); } sb.Remove(offset, len); } else ProcessEntity(); 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_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() 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(); } /// Insert backslashes in front of Markdown reserved characters /// The text to escape /// The escaped text, ready to be used in MarkdownToEntities without problems 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 { /// 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(">", ">"); } }