Skip to content

Commit 79dae45

Browse files
authored
Added the MessageEntity type “date_time”, allowing to show a formatted date and time to the user. (#225)
1 parent 6bdf57a commit 79dae45

File tree

8 files changed

+456
-265
lines changed

8 files changed

+456
-265
lines changed

docs/source/topics/text-formatting.rst

Lines changed: 102 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ Pyrogram uses a custom Markdown dialect for text formatting which adds some uniq
1717
texts easier in both Markdown and HTML. You can send sophisticated text messages and media captions using a
1818
variety of decorations that can also be nested in order to combine multiple styles together.
1919

20+
`The official BOT API style HTML formatting is also supported <https://core.telegram.org/bots/api#html-style>`__
21+
2022

2123
-----
2224

@@ -40,7 +42,97 @@ list of the basic styles currently supported by Pyrogram.
4042
- spoiler
4143
- `text URL <https://telegramplayground.github.io/pyrogram/>`_
4244
- `user text mention <tg://user?id=123456789>`_
45+
- :emoji:`👍`
46+
47+
48+
HTML Style
49+
----------
50+
51+
To strictly use this mode, pass :obj:`~pyrogram.enums.HTML` to the *parse_mode* parameter when using
52+
:meth:`~pyrogram.Client.send_message`. The following tags are currently supported:
53+
54+
.. code-block:: text
55+
56+
<b>bold</b>, <strong>bold</strong>
57+
58+
<i>italic</i>, <em>italic</em>
59+
60+
<u>underline</u>
61+
62+
<s>strike</s>, <del>strike</del>, <strike>strike</strike>
63+
64+
<tg-spoiler>spoiler</tg-spoiler>
65+
66+
<a href="https://telegramplayground.github.io/pyrogram/">text URL</a>
67+
68+
<a href="tg://user?id=123456789">inline mention</a>
69+
70+
<code>inline fixed-width code</code>
71+
72+
<tg-emoji emoji-id="5469770542288478598">👍</tg-emoji>
73+
74+
<pre>
75+
<code class="language-python">
76+
pre-formatted fixed-width code block written in the Python programming language
77+
</code>
78+
</pre>
79+
80+
**Example**:
4381

82+
.. code-block:: python
83+
84+
from pyrogram.enums import ParseMode
85+
86+
await app.send_message(
87+
chat_id="me",
88+
text=(
89+
"<b>bold</b>, <strong>bold</strong>"
90+
"<i>italic</i>, <em>italic</em>"
91+
"<u>underline</u>, <ins>underline</ins>"
92+
"<s>strike</s>, <strike>strike</strike>, <del>strike</del>"
93+
"<tg-spoiler>spoiler</tg-spoiler>\n\n"
94+
95+
"<b>bold <i>italic bold <s>italic bold strike <tg-spoiler>italic bold strike spoiler</tg-spoiler></s> <u>underline italic bold</u></i> bold</b>\n\n"
96+
97+
"<a href=\"https://telegramplayground.github.io/pyrogram/\">inline URL</a> "
98+
"<a href=\"tg://user?id=23122162\">inline mention of a user</a>\n"
99+
"<tg-emoji emoji-id=5469770542288478598>👍</tg-emoji> "
100+
"<code>inline fixed-width code</code> "
101+
"<pre>pre-formatted fixed-width code block</pre>\n\n"
102+
"</pre><code class='language-python'>"
103+
"for i in range(10):\n"
104+
" print(i)"
105+
"</code></pre>\n\n"
106+
107+
"<blockquote>Block quotation started"
108+
"Block quotation continued"
109+
"The last line of the block quotation</blockquote>"
110+
"<blockquote expandable>Expandable block quotation started"
111+
"Expandable block quotation continued"
112+
"Expandable block quotation continued"
113+
"Hidden by default part of the block quotation started"
114+
"Expandable block quotation continued"
115+
"The last line of the block quotation</blockquote>"
116+
),
117+
parse_mode=ParseMode.HTML
118+
)
119+
120+
.. note::
121+
122+
All ``<``, ``>`` and ``&`` symbols that are not a part of a tag or an HTML entity must be replaced with the
123+
corresponding HTML entities (``<`` with ``&lt;``, ``>`` with ``&gt;`` and ``&`` with ``&amp;``). You can use this
124+
snippet to quickly escape those characters:
125+
126+
.. code-block:: python
127+
128+
text = "<my & text>"
129+
text = text.replace("<", "&lt;").replace("&", "&amp;")
130+
131+
print(text)
132+
133+
.. code-block:: text
134+
135+
&lt;my &amp; text>
44136
45137
46138
Markdown Style
@@ -107,11 +199,12 @@ To strictly use this mode, pass :obj:`~pyrogram.enums.ParseMode.MARKDOWN` to the
107199
"~~strike~~, "
108200
"||spoiler||, "
109201
"[URL](https://telegramplayground.github.io/pyrogram/), "
202+
"![👍](tg://emoji?id=5469770542288478598)"
110203
"`code`, "
111-
"```"
204+
"```py"
112205
"for i in range(10):\n"
113206
" print(i)"
114-
"```"
207+
"```\n"
115208
116209
">blockquote\n"
117210
@@ -135,96 +228,6 @@ To strictly use this mode, pass :obj:`~pyrogram.enums.ParseMode.MARKDOWN` to the
135228
parse_mode=ParseMode.MARKDOWN
136229
)
137230
138-
HTML Style
139-
----------
140-
141-
To strictly use this mode, pass :obj:`~pyrogram.enums.HTML` to the *parse_mode* parameter when using
142-
:meth:`~pyrogram.Client.send_message`. The following tags are currently supported:
143-
144-
.. code-block:: text
145-
146-
<b>bold</b>, <strong>bold</strong>
147-
148-
<i>italic</i>, <em>italic</em>
149-
150-
<u>underline</u>
151-
152-
<s>strike</s>, <del>strike</del>, <strike>strike</strike>
153-
154-
<spoiler>spoiler</spoiler>
155-
156-
<a href="https://telegramplayground.github.io/pyrogram/">text URL</a>
157-
158-
<a href="tg://user?id=123456789">inline mention</a>
159-
160-
<code>inline fixed-width code</code>
161-
162-
<emoji id="12345678901234567890">🔥</emoji>
163-
164-
<pre language="py">
165-
pre-formatted
166-
fixed-width
167-
code block
168-
</pre>
169-
170-
**Example**:
171-
172-
.. code-block:: python
173-
174-
from pyrogram.enums import ParseMode
175-
176-
await app.send_message(
177-
chat_id="me",
178-
text=(
179-
"<b>bold</b>, <strong>bold</strong>"
180-
"<i>italic</i>, <em>italic</em>"
181-
"<u>underline</u>, <ins>underline</ins>"
182-
"<s>strike</s>, <strike>strike</strike>, <del>strike</del>"
183-
"<spoiler>spoiler</spoiler>\n\n"
184-
185-
"<b>bold <i>italic bold <s>italic bold strike <spoiler>italic bold strike spoiler</spoiler></s> <u>underline italic bold</u></i> bold</b>\n\n"
186-
187-
"<a href=\"https://telegramplayground.github.io/pyrogram/\">inline URL</a> "
188-
"<a href=\"tg://user?id=23122162\">inline mention of a user</a>\n"
189-
"<emoji id=5368324170671202286>👍</emoji> "
190-
"<code>inline fixed-width code</code> "
191-
"<pre>pre-formatted fixed-width code block</pre>\n\n"
192-
"<pre language='py'>"
193-
"for i in range(10):\n"
194-
" print(i)"
195-
"</pre>\n\n"
196-
197-
"<blockquote>Block quotation started"
198-
"Block quotation continued"
199-
"The last line of the block quotation</blockquote>"
200-
"<blockquote expandable>Expandable block quotation started"
201-
"Expandable block quotation continued"
202-
"Expandable block quotation continued"
203-
"Hidden by default part of the block quotation started"
204-
"Expandable block quotation continued"
205-
"The last line of the block quotation</blockquote>"
206-
),
207-
parse_mode=ParseMode.HTML
208-
)
209-
210-
.. note::
211-
212-
All ``<``, ``>`` and ``&`` symbols that are not a part of a tag or an HTML entity must be replaced with the
213-
corresponding HTML entities (``<`` with ``&lt;``, ``>`` with ``&gt;`` and ``&`` with ``&amp;``). You can use this
214-
snippet to quickly escape those characters:
215-
216-
.. code-block:: python
217-
218-
import html
219-
220-
text = "<my text>"
221-
text = html.escape(text)
222-
223-
print(text)
224-
225-
.. code-block:: text
226-
227-
&lt;my text&gt;
228231
229232
Different Styles
230233
----------------
@@ -272,6 +275,13 @@ Result:
272275
Nested and Overlapping Entities
273276
-------------------------------
274277

278+
.. warning::
279+
280+
The Markdown style is not recommended for complex text formatting.
281+
282+
If you want to use complex text formatting such as nested entities, overlapping entities use the HTML style instead.
283+
284+
275285
You can also style texts with more than one decoration at once by nesting entities together. For example, you can send
276286
a text message with both :bold-underline:`bold and underline` styles, or a text that has both :strike-italic:`italic and
277287
strike` styles, and you can still combine both Markdown and HTML together.

pyrogram/enums/message_entity_type.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,8 @@ class MessageEntityType(AutoName):
8686
BANK_CARD = raw.types.MessageEntityBankCard
8787
"Bank card text"
8888

89+
DATE_TIME = raw.types.MessageEntityFormattedDate
90+
"Formatted date and time"
91+
8992
UNKNOWN = raw.types.MessageEntityUnknown
9093
"Unknown message entity type"

pyrogram/parser/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Pyrogram - Telegram MTProto API Client Library for Python
2-
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
2+
# Copyright (C) 2017-2024 Dan <https://github.com/delivrance>
3+
# Copyright (C) 2026-present <https://github.com/TelegramPlayGround>
34
#
45
# This file is part of Pyrogram.
56
#

pyrogram/parser/html.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Pyrogram - Telegram MTProto API Client Library for Python
2-
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
2+
# Copyright (C) 2017-2024 Dan <https://github.com/delivrance>
3+
# Copyright (C) 2026-present <https://github.com/TelegramPlayGround>
34
#
45
# This file is part of Pyrogram.
56
#
@@ -51,19 +52,34 @@ def handle_starttag(self, tag, attrs):
5152
entity = raw.types.MessageEntityBold
5253
elif tag in ["i", "em"]:
5354
entity = raw.types.MessageEntityItalic
54-
elif tag == "u":
55+
elif tag in ["u", "ins"]:
5556
entity = raw.types.MessageEntityUnderline
5657
elif tag in ["s", "del", "strike"]:
5758
entity = raw.types.MessageEntityStrike
5859
elif tag == "blockquote":
5960
entity = raw.types.MessageEntityBlockquote
6061
extra["collapsed"] = bool("expandable" in attrs.keys())
61-
elif tag == "code":
62-
entity = raw.types.MessageEntityCode
6362
elif tag == "pre":
6463
entity = raw.types.MessageEntityPre
6564
extra["language"] = attrs.get("language", "")
66-
elif tag == "spoiler":
65+
elif tag == "code":
66+
entity = raw.types.MessageEntityCode
67+
_class = attrs.get("class", "")
68+
active_pres = self.tag_entities.get("pre", [])
69+
# Check if this <code> block is nested inside an active <pre>
70+
if active_pres:
71+
if _class.startswith("language-"):
72+
# Update the language of the currently open <pre> entity
73+
active_pres[-1].language = _class[9:] # 9 is the length of "language-"
74+
75+
# Return early to intentionally skip creating a nested CODE entity.
76+
# (When handle_endtag hits </code>, it will safely ignore it).
77+
return
78+
# If not inside a <pre>, treat it as a standard inline code block
79+
entity = raw.types.MessageEntityCode
80+
elif tag in ["spoiler", "tg-spoiler"]:
81+
entity = raw.types.MessageEntitySpoiler
82+
elif tag == "span" and attrs.get("class", "") == "tg-spoiler":
6783
entity = raw.types.MessageEntitySpoiler
6884
elif tag == "a":
6985
url = attrs.get("href", "")
@@ -80,6 +96,15 @@ def handle_starttag(self, tag, attrs):
8096
entity = raw.types.MessageEntityCustomEmoji
8197
custom_emoji_id = int(attrs.get("id"))
8298
extra["document_id"] = custom_emoji_id
99+
elif tag == "tg-emoji":
100+
entity = raw.types.MessageEntityCustomEmoji
101+
custom_emoji_id = int(attrs.get("emoji-id"))
102+
extra["document_id"] = custom_emoji_id
103+
elif tag == "tg-time":
104+
entity = raw.types.MessageEntityFormattedDate
105+
extra["date"] = int(attrs.get("unix"))
106+
date_time_format = attrs.get("format", "")
107+
extra = utils.parse_date_time_format_tl(extra, date_time_format)
83108
else:
84109
return
85110

@@ -176,18 +201,15 @@ def parse_one(entity):
176201
elif entity_type == MessageEntityType.PRE:
177202
name = entity_type.name.lower()
178203
language = getattr(entity, "language", "") or ""
179-
start_tag = f'<{name} language="{language}">' if language else f"<{name}>"
180-
end_tag = f"</{name}>"
181-
elif entity_type == MessageEntityType.BLOCKQUOTE:
182-
name = entity_type.name.lower()
183-
start_tag = f"<{name}>"
184-
end_tag = f"</{name}>"
204+
start_tag = f'<pre><code class="language-{language}">' if language else f"<{name}>"
205+
end_tag = f"</code></pre>" if language else f"</{name}>"
185206
elif entity_type == MessageEntityType.EXPANDABLE_BLOCKQUOTE:
186207
name = "blockquote"
187208
start_tag = f"<{name} expandable>"
188209
end_tag = f"</{name}>"
189210
elif entity_type in (
190211
MessageEntityType.CODE,
212+
MessageEntityType.BLOCKQUOTE,
191213
MessageEntityType.SPOILER,
192214
):
193215
name = entity_type.name.lower()
@@ -203,8 +225,13 @@ def parse_one(entity):
203225
end_tag = "</a>"
204226
elif entity_type == MessageEntityType.CUSTOM_EMOJI:
205227
custom_emoji_id = entity.custom_emoji_id
206-
start_tag = f'<emoji id="{custom_emoji_id}">'
228+
start_tag = f'<tg-emoji emoji-id="{custom_emoji_id}">'
207229
end_tag = "</emoji>"
230+
elif entity_type == MessageEntityType.DATE_TIME:
231+
date = entity.unix_time
232+
dtf = entity.date_time_format
233+
start_tag = f'<tg-time unix="{date}" format="{dtf}">'
234+
end_tag = "</tg-time>"
208235
else:
209236
return
210237

0 commit comments

Comments
 (0)