Skip to content

Commit 24dbbcf

Browse files
committed
Html/Markdown conversions:
- Support for MessageEntityFormattedDate - Removed premium optional arg (always true now) - Stopped supporting non-standard markdown "emoji?id=". Correct syntax is "tg://emoji?id="
1 parent 6611e86 commit 24dbbcf

2 files changed

Lines changed: 43 additions & 19 deletions

File tree

EXAMPLES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ await Task.Delay(5000);
365365
```csharp
366366
// • Sending a message with custom emojies in Markdown to ourself:
367367
var text = "Vicksy says Hi! ![👋](tg://emoji?id=5190875290439525089)";
368-
var entities = client.MarkdownToEntities(ref text, premium: true);
368+
var entities = client.MarkdownToEntities(ref text);
369369
await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
370370
// also available in HTML: <tg-emoji emoji-id="5190875290439525089">👋</tg-emoji>
371371

src/Services.cs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,9 @@ public static class Markdown
131131
/// <summary>Converts a <a href="https://core.telegram.org/bots/api/#markdownv2-style">Markdown text</a> into the (plain text + entities) format used by Telegram messages</summary>
132132
/// <param name="_">not used anymore, you can pass null</param>
133133
/// <param name="text">[in] The Markdown text<br/>[out] The same (plain) text, stripped of all Markdown notation</param>
134-
/// <param name="premium">Generate premium entities if any</param>
135134
/// <param name="users">Dictionary used for <c>tg://user?id=</c> notation</param>
136135
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="Client.SendMessageAsync">SendMessageAsync</see> or <see cref="Client.SendMediaAsync">SendMediaAsync</see></returns>
137-
public static MessageEntity[] MarkdownToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary<long, User> users = null)
136+
public static MessageEntity[] MarkdownToEntities(this Client _, ref string text, IReadOnlyDictionary<long, User> users = null)
138137
{
139138
var entities = new List<MessageEntity>();
140139
MessageEntityBlockquote lastBlockQuote = null;
@@ -217,12 +216,18 @@ public static MessageEntity[] MarkdownToEntities(this Client _, ref string text,
217216
else if (c == ')') break;
218217
}
219218
textUrl.url = sb.ToString(offset + 2, offset2 - offset - 3);
219+
sb.Remove(offset, offset2 - offset);
220220
if (textUrl.url.StartsWith("tg://user?id=") && long.TryParse(textUrl.url[13..], out var id) && users?.GetValueOrDefault(id)?.access_hash is long hash)
221221
entities[lastIndex] = new InputMessageEntityMentionName { offset = textUrl.offset, length = textUrl.length, user_id = new InputUser(id, hash) };
222-
else if ((textUrl.url.StartsWith("tg://emoji?id=") || textUrl.url.StartsWith("emoji?id=")) && long.TryParse(textUrl.url[(textUrl.url.IndexOf('=') + 1)..], out id))
223-
if (premium) entities[lastIndex] = new MessageEntityCustomEmoji { offset = textUrl.offset, length = textUrl.length, document_id = id };
224-
else entities.RemoveAt(lastIndex);
225-
sb.Remove(offset, offset2 - offset);
222+
else if (textUrl.url.StartsWith("tg://emoji?id=") && long.TryParse(textUrl.url[14..], out id))
223+
entities[lastIndex] = new MessageEntityCustomEmoji { offset = textUrl.offset, length = textUrl.length, document_id = id };
224+
else if (textUrl.url.StartsWith("tg://time?unix=") && textUrl.url.IndexOf("&format=", 15) is { } idxFormat)
225+
entities[lastIndex] = new MessageEntityFormattedDate
226+
{
227+
offset = textUrl.offset, length = textUrl.length,
228+
date = new DateTime((long.Parse(idxFormat < 0 ? textUrl.url[15..] : textUrl.url[15..idxFormat]) + 62135596800L) * 10000000, DateTimeKind.Utc),
229+
flags = idxFormat < 0 ? 0 : HtmlText.DateFlags(textUrl.url[(idxFormat + 8)..])
230+
};
226231
break;
227232
}
228233
}
@@ -264,9 +269,8 @@ void CloseBlockQuote()
264269
/// <param name="client">Client, used only for getting current user ID in case of <c>InputMessageEntityMentionName+InputUserSelf</c></param>
265270
/// <param name="message">The plain text, typically obtained from <see cref="Message.message"/></param>
266271
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="Message.entities"/></param>
267-
/// <param name="premium">Convert premium entities (might lead to non-standard markdown)</param>
268272
/// <returns>The message text with MarkdownV2 formattings</returns>
269-
public static string EntitiesToMarkdown(this Client client, string message, MessageEntity[] entities, bool premium = false)
273+
public static string EntitiesToMarkdown(this Client client, string message, MessageEntity[] entities)
270274
{
271275
if (entities == null || entities.Length == 0) return Escape(message);
272276
var closings = new List<(int offset, string md)>();
@@ -301,8 +305,9 @@ public static string EntitiesToMarkdown(this Client client, string message, Mess
301305
else if (nextEntity is InputMessageEntityMentionName imemn)
302306
closing.md = $"](tg://user?id={imemn.user_id.UserId ?? client.UserId})";
303307
else if (nextEntity is MessageEntityCustomEmoji mecu)
304-
if (premium) closing.md = $"](tg://emoji?id={mecu.document_id})";
305-
else continue;
308+
closing.md = $"](tg://emoji?id={mecu.document_id})";
309+
else if (nextEntity is MessageEntityFormattedDate mefd)
310+
closing.md = $"](tg://time?unix={((DateTimeOffset)mefd.date).ToUnixTimeSeconds()}{(mefd.flags == 0 ? null : $"&format={HtmlText.DateFormat(mefd.flags)}")})";
306311
}
307312
else if (nextEntity is MessageEntityBlockquote mebq)
308313
{ inBlockQuote = true; if (lastCh is not '\n' and not '\0') md = "\n>";
@@ -343,6 +348,7 @@ public static string EntitiesToMarkdown(this Client client, string message, Mess
343348
[typeof(MessageEntitySpoiler)] = "||",
344349
[typeof(MessageEntityCustomEmoji)] = "![",
345350
[typeof(MessageEntityBlockquote)] = ">",
351+
[typeof(MessageEntityFormattedDate)] = "![",
346352
};
347353

348354
/// <summary>Insert backslashes in front of Markdown reserved characters</summary>
@@ -372,10 +378,9 @@ public static class HtmlText
372378
/// <summary>Converts an <a href="https://core.telegram.org/bots/api/#html-style">HTML-formatted text</a> into the (plain text + entities) format used by Telegram messages</summary>
373379
/// <param name="_">not used anymore, you can pass null</param>
374380
/// <param name="text">[in] The HTML-formatted text<br/>[out] The same (plain) text, stripped of all HTML tags</param>
375-
/// <param name="premium">Generate premium entities if any</param>
376381
/// <param name="users">Dictionary used for <c>tg://user?id=</c> notation</param>
377382
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="Client.SendMessageAsync">SendMessageAsync</see> or <see cref="Client.SendMediaAsync">SendMediaAsync</see></returns>
378-
public static MessageEntity[] HtmlToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary<long, User> users = null)
383+
public static MessageEntity[] HtmlToEntities(this Client _, ref string text, IReadOnlyDictionary<long, User> users = null)
379384
{
380385
var entities = new List<MessageEntity>();
381386
var sb = new StringBuilder(text);
@@ -419,6 +424,7 @@ public static MessageEntity[] HtmlToEntities(this Client _, ref string text, boo
419424
case "code": ProcessEntity<MessageEntityCode>(); break;
420425
case "pre": ProcessEntity<MessageEntityPre>(); break;
421426
case "tg-emoji" when closing: ProcessEntity<MessageEntityCustomEmoji>(); break;
427+
case "tg-time" when closing: ProcessEntity<MessageEntityFormattedDate>(); break;
422428
case "blockquote": ProcessEntity<MessageEntityBlockquote>(); break;
423429
case "blockquote expandable":
424430
entities.Add(new MessageEntityBlockquote { offset = offset, length = -1, flags = MessageEntityBlockquote.Flags.collapsed });
@@ -448,8 +454,15 @@ public static MessageEntity[] HtmlToEntities(this Client _, ref string text, boo
448454
if (entities.LastOrDefault(e => e.length == -1) is MessageEntityPre prevEntity)
449455
prevEntity.language = tag[21..^1];
450456
}
451-
else if (premium && (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji emoji-id='")))
452-
entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[(tag.IndexOf('=') + 2)..^1]) });
457+
else if (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji emoji-id='"))
458+
entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[19..^1]) });
459+
else if ((tag.StartsWith("tg-time unix=\"") || tag.StartsWith("tg-time unix='")) && (end = tag.IndexOf(tag[13], 14)) > 0)
460+
entities.Add(new MessageEntityFormattedDate
461+
{
462+
offset = offset, length = -1,
463+
date = new DateTime((long.Parse(tag[14..end]) + 62135596800L) * 10000000, DateTimeKind.Utc),
464+
flags = string.Compare(tag, end + 1, " format=", 0, 8) == 0 ? DateFlags(tag[(end + 10)..^1]) : 0
465+
});
453466
break;
454467
}
455468

@@ -486,9 +499,8 @@ internal static void FixUps(StringBuilder sb, List<MessageEntity> entities)
486499
/// <param name="client">Client, used only for getting current user ID in case of <c>InputMessageEntityMentionName+InputUserSelf</c></param>
487500
/// <param name="message">The plain text, typically obtained from <see cref="Message.message"/></param>
488501
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="Message.entities"/></param>
489-
/// <param name="premium">Convert premium entities</param>
490502
/// <returns>The message text with HTML formatting tags</returns>
491-
public static string EntitiesToHtml(this Client client, string message, MessageEntity[] entities, bool premium = false)
503+
public static string EntitiesToHtml(this Client client, string message, MessageEntity[] entities)
492504
{
493505
if (entities == null || entities.Length == 0) return Escape(message);
494506
var closings = new List<(int offset, string tag)>();
@@ -519,15 +531,16 @@ public static string EntitiesToHtml(this Client client, string message, MessageE
519531
tag = $"<a href=\"tg://user?id={imemn.user_id.UserId ?? client.UserId}\">";
520532
}
521533
else if (nextEntity is MessageEntityCustomEmoji mecu)
522-
if (premium) tag = $"<tg-emoji emoji-id=\"{mecu.document_id}\">";
523-
else continue;
534+
tag = $"<tg-emoji emoji-id=\"{mecu.document_id}\">";
524535
else if (nextEntity is MessageEntityPre mep && !string.IsNullOrEmpty(mep.language))
525536
{
526537
closing.Item2 = "</code></pre>";
527538
tag = $"<pre><code class=\"language-{mep.language}\">";
528539
}
529540
else if (nextEntity is MessageEntityBlockquote { flags: MessageEntityBlockquote.Flags.collapsed })
530541
tag = "<blockquote expandable>";
542+
else if (nextEntity is MessageEntityFormattedDate mefd)
543+
tag = $"<tg-time unix=\"{((DateTimeOffset)mefd.date).ToUnixTimeSeconds()}\"{(mefd.flags == 0 ? null : $" format=\"{DateFormat(mefd.flags)}\"")}>";
531544
else
532545
tag = $"<{tag}>";
533546
int index = ~closings.BinarySearch(closing, Comparer<(int, string)>.Create((x, y) => x.Item1.CompareTo(y.Item1) | 1));
@@ -559,12 +572,23 @@ public static string EntitiesToHtml(this Client client, string message, MessageE
559572
[typeof(MessageEntitySpoiler)] = "tg-spoiler",
560573
[typeof(MessageEntityCustomEmoji)] = "tg-emoji",
561574
[typeof(MessageEntityBlockquote)] = "blockquote",
575+
[typeof(MessageEntityFormattedDate)] = "tg-time",
562576
};
563577

564578
/// <summary>Replace special HTML characters with their &amp;xx; equivalent</summary>
565579
/// <param name="text">The text to make HTML-safe</param>
566580
/// <returns>The HTML-safe text, ready to be used in <see cref="HtmlToEntities">HtmlToEntities</see> without problems</returns>
567581
public static string Escape(string text)
568582
=> text?.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
583+
584+
internal static string DateFormat(MessageEntityFormattedDate.Flags flags) => flags.HasFlag(MessageEntityFormattedDate.Flags.relative) ? "r" :
585+
((flags & MessageEntityFormattedDate.Flags.day_of_week) != 0 ? "w" : "") +
586+
((flags & MessageEntityFormattedDate.Flags.short_date) != 0 ? "d" : "") +
587+
((flags & MessageEntityFormattedDate.Flags.long_date) != 0 ? "D" : "") +
588+
((flags & MessageEntityFormattedDate.Flags.short_time) != 0 ? "t" : "") +
589+
((flags & MessageEntityFormattedDate.Flags.long_time) != 0 ? "T" : "");
590+
591+
internal static MessageEntityFormattedDate.Flags DateFlags(string format)
592+
=> (MessageEntityFormattedDate.Flags)format.Sum(c => 1 << "rtTdDw".IndexOf(c));
569593
}
570594
}

0 commit comments

Comments
 (0)