diff --git a/EXAMPLES.md b/EXAMPLES.md index ac2daf1..d00c4b5 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -365,7 +365,7 @@ await Task.Delay(5000); ```csharp // • Sending a message with custom emojies in Markdown to ourself: var text = "Vicksy says Hi! ![👋](tg://emoji?id=5190875290439525089)"; -var entities = client.MarkdownToEntities(ref text, premium: true); +var entities = client.MarkdownToEntities(ref text); await client.SendMessageAsync(InputPeer.Self, text, entities: entities); // also available in HTML: 👋 diff --git a/src/Services.cs b/src/Services.cs index 7d452f2..ebf7733 100644 --- a/src/Services.cs +++ b/src/Services.cs @@ -131,10 +131,9 @@ namespace TL /// Converts a Markdown text into the (plain text + entities) format used by Telegram messages /// not used anymore, you can pass null /// [in] The Markdown text
[out] The same (plain) text, stripped of all Markdown notation - /// Generate premium entities if any /// Dictionary used for tg://user?id= notation /// The array of formatting entities that you can pass (along with the plain text) to SendMessageAsync or SendMediaAsync - public static MessageEntity[] MarkdownToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary users = null) + public static MessageEntity[] MarkdownToEntities(this Client _, ref string text, IReadOnlyDictionary users = null) { var entities = new List(); MessageEntityBlockquote lastBlockQuote = null; @@ -217,12 +216,18 @@ namespace TL else if (c == ')') break; } textUrl.url = sb.ToString(offset + 2, offset2 - offset - 3); + sb.Remove(offset, offset2 - offset); if (textUrl.url.StartsWith("tg://user?id=") && long.TryParse(textUrl.url[13..], out var id) && users?.GetValueOrDefault(id)?.access_hash 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); + else if (textUrl.url.StartsWith("tg://emoji?id=") && long.TryParse(textUrl.url[14..], out id)) + entities[lastIndex] = new MessageEntityCustomEmoji { offset = textUrl.offset, length = textUrl.length, document_id = id }; + else if (textUrl.url.StartsWith("tg://time?unix=") && textUrl.url.IndexOf("&format=", 15) is { } idxFormat) + entities[lastIndex] = new MessageEntityFormattedDate + { + offset = textUrl.offset, length = textUrl.length, + date = new DateTime((long.Parse(idxFormat < 0 ? textUrl.url[15..] : textUrl.url[15..idxFormat]) + 62135596800L) * 10000000, DateTimeKind.Utc), + flags = idxFormat < 0 ? 0 : HtmlText.DateFlags(textUrl.url[(idxFormat + 8)..]) + }; break; } } @@ -264,9 +269,8 @@ namespace TL /// 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 Client client, string message, MessageEntity[] entities, bool premium = false) + public static string EntitiesToMarkdown(this Client client, string message, MessageEntity[] entities) { if (entities == null || entities.Length == 0) return Escape(message); var closings = new List<(int offset, string md)>(); @@ -301,8 +305,9 @@ namespace TL 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; + closing.md = $"](tg://emoji?id={mecu.document_id})"; + else if (nextEntity is MessageEntityFormattedDate mefd) + closing.md = $"](tg://time?unix={((DateTimeOffset)mefd.date).ToUnixTimeSeconds()}{(mefd.flags == 0 ? null : $"&format={HtmlText.DateFormat(mefd.flags)}")})"; } else if (nextEntity is MessageEntityBlockquote mebq) { inBlockQuote = true; if (lastCh is not '\n' and not '\0') md = "\n>"; @@ -343,6 +348,7 @@ namespace TL [typeof(MessageEntitySpoiler)] = "||", [typeof(MessageEntityCustomEmoji)] = "![", [typeof(MessageEntityBlockquote)] = ">", + [typeof(MessageEntityFormattedDate)] = "![", }; /// Insert backslashes in front of Markdown reserved characters @@ -372,10 +378,9 @@ namespace TL /// Converts an HTML-formatted text into the (plain text + entities) format used by Telegram messages /// not used anymore, you can pass null /// [in] The HTML-formatted text
[out] The same (plain) text, stripped of all HTML tags - /// Generate premium entities if any /// Dictionary used for tg://user?id= notation /// The array of formatting entities that you can pass (along with the plain text) to SendMessageAsync or SendMediaAsync - public static MessageEntity[] HtmlToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary users = null) + public static MessageEntity[] HtmlToEntities(this Client _, ref string text, IReadOnlyDictionary users = null) { var entities = new List(); var sb = new StringBuilder(text); @@ -419,6 +424,7 @@ namespace TL case "code": ProcessEntity(); break; case "pre": ProcessEntity(); break; case "tg-emoji" when closing: ProcessEntity(); break; + case "tg-time" when closing: ProcessEntity(); break; case "blockquote": ProcessEntity(); break; case "blockquote expandable": entities.Add(new MessageEntityBlockquote { offset = offset, length = -1, flags = MessageEntityBlockquote.Flags.collapsed }); @@ -448,8 +454,15 @@ namespace TL 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 emoji-id='"))) - entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[(tag.IndexOf('=') + 2)..^1]) }); + else if (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji emoji-id='")) + entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[19..^1]) }); + else if ((tag.StartsWith("tg-time unix=\"") || tag.StartsWith("tg-time unix='")) && (end = tag.IndexOf(tag[13], 14)) > 0) + entities.Add(new MessageEntityFormattedDate + { + offset = offset, length = -1, + date = new DateTime((long.Parse(tag[14..end]) + 62135596800L) * 10000000, DateTimeKind.Utc), + flags = string.Compare(tag, end + 1, " format=", 0, 8) == 0 ? DateFlags(tag[(end + 10)..^1]) : 0 + }); break; } @@ -486,9 +499,8 @@ namespace TL /// 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 Client client, string message, MessageEntity[] entities, bool premium = false) + public static string EntitiesToHtml(this Client client, string message, MessageEntity[] entities) { if (entities == null || entities.Length == 0) return Escape(message); var closings = new List<(int offset, string tag)>(); @@ -519,8 +531,7 @@ namespace TL tag = $""; } else if (nextEntity is MessageEntityCustomEmoji mecu) - if (premium) tag = $""; - else continue; + tag = $""; else if (nextEntity is MessageEntityPre mep && !string.IsNullOrEmpty(mep.language)) { closing.Item2 = ""; @@ -528,6 +539,8 @@ namespace TL } else if (nextEntity is MessageEntityBlockquote { flags: MessageEntityBlockquote.Flags.collapsed }) tag = "
"; + else if (nextEntity is MessageEntityFormattedDate mefd) + tag = $""; else tag = $"<{tag}>"; int index = ~closings.BinarySearch(closing, Comparer<(int, string)>.Create((x, y) => x.Item1.CompareTo(y.Item1) | 1)); @@ -559,6 +572,7 @@ namespace TL [typeof(MessageEntitySpoiler)] = "tg-spoiler", [typeof(MessageEntityCustomEmoji)] = "tg-emoji", [typeof(MessageEntityBlockquote)] = "blockquote", + [typeof(MessageEntityFormattedDate)] = "tg-time", }; /// Replace special HTML characters with their &xx; equivalent @@ -566,5 +580,15 @@ namespace TL /// The HTML-safe text, ready to be used in HtmlToEntities without problems public static string Escape(string text) => text?.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + + internal static string DateFormat(MessageEntityFormattedDate.Flags flags) => flags.HasFlag(MessageEntityFormattedDate.Flags.relative) ? "r" : + ((flags & MessageEntityFormattedDate.Flags.day_of_week) != 0 ? "w" : "") + + ((flags & MessageEntityFormattedDate.Flags.short_date) != 0 ? "d" : "") + + ((flags & MessageEntityFormattedDate.Flags.long_date) != 0 ? "D" : "") + + ((flags & MessageEntityFormattedDate.Flags.short_time) != 0 ? "t" : "") + + ((flags & MessageEntityFormattedDate.Flags.long_time) != 0 ? "T" : ""); + + internal static MessageEntityFormattedDate.Flags DateFlags(string format) + => (MessageEntityFormattedDate.Flags)format.Sum(c => 1 << "rtTdDw".IndexOf(c)); } }