using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; 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 Task Messages_GetChats(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllChats"); public static Task Channels_GetChannels(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllChats"); public static Task Users_GetUsers(this WTelegram.Client _) => throw new ApplicationException("The method you're looking for is Messages_GetAllDialogs"); public static Task Messages_GetMessages(this WTelegram.Client _) => throw new ApplicationException("If you want to get the messages from a chat, use Messages_GetHistory"); } public static class Markdown { /// Converts a Markdown text into the (plain text + entities) 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 /// Generate premium entities if any /// 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, bool premium = false) { var entities = new List(); var sb = new StringBuilder(text); for (int offset = 0; offset < sb.Length;) { switch (sb[offset]) { case '\r': sb.Remove(offset, 1); break; 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) }); if (sb[offset + len] == '\n') len++; } sb.Remove(offset, len); } else ProcessEntity(); break; case '!' when offset + 1 < sb.Length && sb[offset + 1] == '[': sb.Remove(offset, 1); goto case '['; 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 id) && client.GetAccessHashFor(id) is long hash) entities[lastIndex] = new InputMessageEntityMentionName { offset = textUrl.offset, length = textUrl.length, user_id = new InputUser(id, hash) }; else if ((textUrl.url.StartsWith("tg://emoji?id=") || textUrl.url.StartsWith("emoji?id=")) && long.TryParse(textUrl.url[(textUrl.url.IndexOf('=') + 1)..], out id)) if (premium) entities[lastIndex] = new MessageEntityCustomEmoji { offset = textUrl.offset, length = textUrl.length, document_id = id }; else entities.RemoveAt(lastIndex); 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(); } /// Converts the (plain text + entities) format used by Telegram messages into a Markdown text /// Client, used only for getting current user ID in case of InputMessageEntityMentionName+InputUserSelf /// The plain text, typically obtained from /// The array of formatting entities, typically obtained from /// Convert premium entities (might lead to non-standard markdown) /// The message text with MarkdownV2 formattings public static string EntitiesToMarkdown(this WTelegram.Client client, string message, MessageEntity[] entities, bool premium = false) { 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; for (; offset == nextEntity?.offset; nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null) { if (entityToMD.TryGetValue(nextEntity.GetType(), out var md)) { var closing = (nextEntity.offset + nextEntity.length, md); if (md[0] is '[' or '!') { if (nextEntity is MessageEntityTextUrl metu) closing.md = $"]({metu.url.Replace("\\", "\\\\").Replace(")", "\\)").Replace(">", "%3E")})"; 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 MessageEntityCustomEmoji mecu) if (premium) closing.md = $"](tg://emoji?id={mecu.document_id})"; else continue; } 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; } } 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 entityToMD = new() { [typeof(MessageEntityBold)] = "*", [typeof(MessageEntityItalic)] = "_", [typeof(MessageEntityCode)] = "`", [typeof(MessageEntityPre)] = "```", [typeof(MessageEntityTextUrl)] = "[", [typeof(MessageEntityMentionName)] = "[", [typeof(InputMessageEntityMentionName)] = "[", [typeof(MessageEntityUnderline)] = "__", [typeof(MessageEntityStrike)] = "~", [typeof(MessageEntitySpoiler)] = "||", [typeof(MessageEntityCustomEmoji)] = "![", }; /// 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 (plain text + entities) 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 /// Generate premium entities if any /// 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, bool premium = false) { 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; case "tg-emoji" when closing: 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, 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]; } else if (premium && (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji id=\""))) entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[(tag.IndexOf('=') + 2)..^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(); } /// Converts the (plain text + entities) format used by Telegram messages into an HTML-formatted text /// Client, used only for getting current user ID in case of InputMessageEntityMentionName+InputUserSelf /// The plain text, typically obtained from /// The array of formatting entities, typically obtained from /// Convert premium entities /// The message text with HTML formatting tags public static string EntitiesToHtml(this WTelegram.Client client, string message, MessageEntity[] entities, bool premium = false) { 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; for (; offset == nextEntity?.offset; nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null) { if (entityToTag.TryGetValue(nextEntity.GetType(), out var tag)) { var closing = (nextEntity.offset + nextEntity.length, $""); if (tag[0] == 'a') { if (nextEntity is MessageEntityTextUrl metu) tag = $""; else if (nextEntity is MessageEntityMentionName memn) tag = $""; else if (nextEntity is InputMessageEntityMentionName imemn) tag = $""; } else if (nextEntity is MessageEntityCustomEmoji mecu) if (premium) tag = $""; else continue; else if (nextEntity is MessageEntityPre mep && !string.IsNullOrEmpty(mep.language)) { closing.Item2 = ""; tag = $"
";
						}
						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;
					}
				}
				switch (sb[i])
				{
					case '&': sb.Insert(i + 1, "amp;"); i += 4; break;
					case '<': sb.Insert(i, "<"); sb[i += 3] = ';'; break;
					case '>': sb.Insert(i, ">"); sb[i += 3] = ';'; break;
				}
			}
			return sb.ToString();
		}

		static readonly Dictionary 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",
			[typeof(MessageEntityCustomEmoji)] = "tg-emoji",
		};

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