@@ -117,3 +117,169 @@ async def test_edit_rich_both_premium_cases():
117117 result = json .loads (await messages ._edit_rich (no , "peer" , 7 , "new" , "rich" ))
118118 assert result ["reason" ] == "telegram_premium_required"
119119 assert no .requests == []
120+
121+
122+ # --- reading rich messages -------------------------------------------------
123+ # A channel posting in the block format leaves msg.message empty and puts every
124+ # word into msg.rich_message, so such a post used to read back as "[empty]".
125+
126+ types = runtime .types
127+
128+
129+ def _msg (** overrides ):
130+ base = dict (
131+ id = 224 ,
132+ sender = None ,
133+ sender_id = 42 ,
134+ date = "2026-09-03" ,
135+ message = "" ,
136+ reply_to = None ,
137+ rich_message = None ,
138+ )
139+ base .update (overrides )
140+ return SimpleNamespace (** base )
141+
142+
143+ def _plain (text ):
144+ return types .TextPlain (text = text )
145+
146+
147+ def _rich (* blocks ):
148+ return types .RichMessage (blocks = list (blocks ), photos = [], documents = [])
149+
150+
151+ def _cell (text ):
152+ return types .PageTableCell (
153+ header = False ,
154+ align_center = False ,
155+ align_right = False ,
156+ valign_middle = False ,
157+ valign_bottom = False ,
158+ text = _plain (text ),
159+ colspan = None ,
160+ rowspan = None ,
161+ )
162+
163+
164+ def _post_like_agents_lab_224 ():
165+ """The shape a real block-format post arrives in: a heading carrying a
166+ custom emoji, an uncaptioned photo, then paragraphs."""
167+ return _rich (
168+ types .PageBlockHeading1 (
169+ text = types .TextConcat (
170+ texts = [
171+ _plain ("Личная CRM " ),
172+ types .TextCustomEmoji (document_id = 5927026418616636353 , alt = "🧠" ),
173+ ]
174+ )
175+ ),
176+ types .PageBlockPhoto (
177+ photo_id = 5197653938998550234 ,
178+ caption = types .PageCaption (text = types .TextEmpty (), credit = types .TextEmpty ()),
179+ spoiler = False ,
180+ url = None ,
181+ webpage_id = None ,
182+ ),
183+ types .PageBlockParagraph (text = _plain ("Первый абзац." )),
184+ types .PageBlockParagraph (text = _plain ("Второй абзац." )),
185+ )
186+
187+
188+ def test_rich_text_flattens_every_nesting_shape ():
189+ node = types .TextConcat (
190+ texts = [
191+ _plain ("plain " ),
192+ types .TextBold (text = _plain ("bold " )),
193+ types .TextUrl (text = _plain ("link" ), url = "https://example.com" , webpage_id = 0 ),
194+ types .TextCustomEmoji (document_id = 1 , alt = "🧠" ),
195+ types .TextEmpty (),
196+ ]
197+ )
198+
199+ assert runtime .rich_text_to_str (node ) == "plain bold link🧠"
200+
201+
202+ def test_rich_text_ignores_nodes_that_carry_no_text ():
203+ assert runtime .rich_text_to_str (types .TextEmpty ()) == ""
204+ assert runtime .rich_text_to_str (types .TextImage (document_id = 1 , w = 10 , h = 10 )) == ""
205+ assert runtime .rich_text_to_str (None ) == ""
206+
207+
208+ def test_rich_message_text_walks_blocks ():
209+ text = runtime .rich_message_text (_msg (rich_message = _post_like_agents_lab_224 ()))
210+
211+ # One paragraph per block; the empty photo caption contributes nothing.
212+ assert text == "Личная CRM 🧠\n \n Первый абзац.\n \n Второй абзац."
213+
214+
215+ def test_rich_message_text_reaches_captions_lists_tables_and_details ():
216+ rich = _rich (
217+ types .PageBlockVideo (
218+ video_id = 1 ,
219+ caption = types .PageCaption (text = _plain ("под видео" ), credit = types .TextEmpty ()),
220+ autoplay = False ,
221+ loop = False ,
222+ spoiler = False ,
223+ ),
224+ types .PageBlockList (
225+ items = [
226+ types .PageListItemText (text = _plain ("первый" ), checkbox = None , checked = None ),
227+ types .PageListItemBlocks (
228+ blocks = [types .PageBlockParagraph (text = _plain ("второй" ))],
229+ checkbox = None ,
230+ checked = None ,
231+ ),
232+ ]
233+ ),
234+ types .PageBlockTable (
235+ title = types .TextEmpty (),
236+ rows = [types .PageTableRow (cells = [_cell ("A" ), _cell ("B" )])],
237+ bordered = False ,
238+ striped = False ,
239+ ),
240+ types .PageBlockDetails (
241+ blocks = [types .PageBlockParagraph (text = _plain ("скрытое" ))],
242+ title = _plain ("подробнее" ),
243+ open = False ,
244+ ),
245+ )
246+
247+ assert runtime .rich_message_text (_msg (rich_message = rich )) == (
248+ "под видео\n \n первый\n второй\n \n A | B\n \n подробнее\n скрытое"
249+ )
250+
251+
252+ def test_rich_message_text_skips_unknown_blocks_instead_of_failing ():
253+ rich = _rich (
254+ SimpleNamespace (), # a block type Telegram adds after this telethon build
255+ types .PageBlockParagraph (text = _plain ("уцелевший абзац" )),
256+ )
257+
258+ assert runtime .rich_message_text (_msg (rich_message = rich )) == "уцелевший абзац"
259+
260+
261+ def test_rich_message_text_is_empty_without_a_rich_message ():
262+ assert runtime .rich_message_text (_msg ()) == ""
263+ assert runtime .rich_message_text (SimpleNamespace ()) == ""
264+
265+
266+ def test_message_to_dict_falls_back_to_rich_message ():
267+ d = messages .message_to_dict (_msg (rich_message = _post_like_agents_lab_224 ()))
268+
269+ assert d ["text" ].startswith ("Личная CRM 🧠" )
270+ assert d ["rich" ] is True
271+
272+
273+ def test_format_message_line_falls_back_to_rich_message ():
274+ line = messages .format_message_line (_msg (rich_message = _post_like_agents_lab_224 ()))
275+
276+ assert "Message: [empty]" not in line
277+ assert "Личная CRM 🧠\\ n\\ nПервый абзац." in line
278+ assert "rich" in line
279+
280+
281+ def test_plain_message_text_still_wins_over_rich_message ():
282+ msg = _msg (message = "обычный текст" , rich_message = _post_like_agents_lab_224 ())
283+
284+ assert messages .message_to_dict (msg )["text" ] == "обычный текст"
285+ assert "обычный текст" in messages .format_message_line (msg )
0 commit comments