refactor(render): этап 2 — футер merged-групп из настоящего Message (#29) #38

Merged
agent_vscode merged 1 commits from refactor/render-stage2-footer into main 2026-07-07 16:45:55 +03:00
13 changed files with 279 additions and 141 deletions
+6 -1
View File
@@ -1055,7 +1055,12 @@ class PostParser:
else: emoji = "" # Default for unknown cases
reactions_html += f'<span class="reaction">{emoji} {reaction.count}&nbsp;&nbsp;</span>'
reactions_html = reactions_html.rstrip()
first_line_parts.append(reactions_html)
# An empty reactions object (reactions.reactions == []) produces no
# spans; appending the empty string would emit a leading
# '&nbsp;&nbsp;|&nbsp;&nbsp;' separator in the footer. Skip it
# (registry §3.15). Affects single posts too.
if reactions_html:
first_line_parts.append(reactions_html)
# Add views
if views := getattr(message, "views", None):
+17 -67
View File
@@ -15,7 +15,6 @@ import asyncio
import re
import time
from datetime import datetime, timezone
from types import SimpleNamespace
from typing import Optional
from feedgen.feed import FeedGenerator
from pyrogram import errors, Client
@@ -151,54 +150,6 @@ def _trim_messages_groups(messages_groups: list[list[Message]], limit: int):
return messages_groups
def processed_message_to_tg_message(processed_message: dict) -> Message:
"""
Convert processed message dictionary into a Message-like object
containing only the attributes needed by generate_html_footer.
"""
# Create a simple chat object
chat_info = SimpleNamespace()
channel_identifier = processed_message.get('channel')
if isinstance(channel_identifier, str) and channel_identifier.startswith('-100'):
setattr(chat_info, 'id', int(channel_identifier))
setattr(chat_info, 'username', None)
else:
setattr(chat_info, 'id', None) # Or some placeholder if needed
setattr(chat_info, 'username', channel_identifier)
# Convert reactions dict to list of objects
reactions_list = []
if reactions_dict := processed_message.get('reactions'):
for emoji, count in reactions_dict.items():
# Assuming no custom/paid reactions in this simplified structure
reactions_list.append(SimpleNamespace(emoji=emoji, count=count, is_paid=False, custom_emoji_id=None))
# Recreate reactions structure expected by Pyrogram's reaction handling
reactions_obj = SimpleNamespace(reactions=reactions_list) if reactions_list else None
# Create the message-like object
tg_message_mock = SimpleNamespace(
id=processed_message.get('message_id'),
date=datetime.fromtimestamp(processed_message['date'], tz=timezone.utc) if processed_message.get('date') else None,
views=processed_message.get('views'),
reactions=reactions_obj,
chat=chat_info,
# Add other attributes if generate_html_footer or its dependencies need them
# For now, these seem sufficient based on the analysis of generate_html_footer
# and _reactions_views_links.
text=processed_message.get('text'), # Add text just in case
caption=None, # Assume caption is merged into text by process_message
forward_origin=None, # Not directly needed by footer generation logic itself
reply_to_message=None, # Not directly needed by footer generation logic itself
media=None, # Not needed by footer
service=processed_message.get('service') # Potentially needed? Added just in case.
)
# Cast to Message type hint for static analysis, although it's a mock object
return tg_message_mock # type: ignore
def _render_messages_groups(messages_groups: list[list[Message]],
post_parser: PostParser,
exclude_flags: str | None = None,
@@ -240,12 +191,14 @@ def _render_messages_groups(messages_groups: list[list[Message]],
else: # Multiple messages in group - merge text and html body
processed_messages = [post_parser.process_message(msg, include_raw=False, sanitize=False) for msg in group]
# Determine main message for header/footer/title
main_message = next(
(msg for msg in processed_messages if msg['text']),
processed_messages[0] # fallback if no message contains text
)
# Determine the main message with the SAME criterion the processed dicts
# used: first message that has text or caption, else the first of the
# group. processed_messages[i] corresponds to group[i], so main_message
# (dict, for title/date/author) and main_raw (the real Message, for the
# footer) point at the same index.
main_idx = next((i for i, m in enumerate(group) if (m.text or m.caption)), 0)
main_raw = group[main_idx]
main_message = processed_messages[main_idx]
# Merge text fields from all messages
all_texts = [msg['text'] for msg in processed_messages if msg['text']]
@@ -255,19 +208,16 @@ def _render_messages_groups(messages_groups: list[list[Message]],
all_html_bodies = [msg['html']['body'] for msg in processed_messages if msg['html']['body']]
combined_html_body = '\n<br><br>\n'.join(all_html_bodies)
# Collect all unique flags from all messages in the group
all_flags = set()
for msg in processed_messages:
if msg.get('flags'): # Check if flags exist and are not empty
all_flags.update(msg['flags'])
all_flags.add("merged")
merged_flags = list(all_flags) # Convert back to list if needed, or keep as set
# Deterministic merged flags: first-seen order across the group, then
# 'merged' (registry §3.8 — replaces the hash-ordered list(set(...))).
merged_flags = list(dict.fromkeys(f for msg in processed_messages for f in msg['flags']))
merged_flags.append("merged")
# generate tg-message from processed message
tg_message = processed_message_to_tg_message(main_message)
footer_html = post_parser.generate_html_footer(tg_message, flags_list=merged_flags)
# Render the merged footer DIRECTLY from the real main Message — no
# dict->mock round-trip. The raw Message carries its real reactions
# (custom emojis get their own span, registry §3.6) and its naive-local
# date (registry §3.7), matching a single post of the same message.
footer_html = post_parser.generate_html_footer(main_raw, flags_list=merged_flags)
html_parts = [
f'<div class="message-body">{combined_html_body}</div>',
+6 -12
View File
@@ -118,27 +118,21 @@ def capture_html(channel):
_LASTBUILDDATE_RE = re.compile(r"<lastBuildDate>.*?</lastBuildDate>", re.DOTALL)
# feedgen 1.0.0 emits no <generator>; normalized anyway as cheap insurance vs. a lib upgrade.
_GENERATOR_RE = re.compile(r"<generator>.*?</generator>", re.DOTALL)
# Merged-post flags are built as list(set(...)) (rss_generator.py:260-265): order depends on
# PYTHONHASHSEED, which conftest cannot pin. Sort the flag tokens inside each flags div.
# (This normalization is removed by the stage-2 commit referencing §3.8.)
_FLAGS_DIV_RE = re.compile(r'<div class="message-flags">(.*?)</div>', re.DOTALL)
def _sort_flags_div(match):
flags = sorted(p.strip() for p in match.group(1).split("🏷") if p.strip())
inner = " ".join("🏷 " + f for f in flags)
return f'<div class="message-flags"> {inner} </div>'
# NOTE: the stage-0 flag-sort normalization (_FLAGS_DIV_RE / _sort_flags_div) was
# removed in stage 2 (§3.8). Merged-post flags are now built in deterministic
# first-seen order (rss_generator._render_messages_groups: dict.fromkeys(...)),
# so the golden stores the real order and no normalization is needed — keeping it
# would only mask a real flag-order regression.
def normalize_rss(xml):
xml = _LASTBUILDDATE_RE.sub("<lastBuildDate/>", xml)
xml = _GENERATOR_RE.sub("<generator/>", xml)
xml = _FLAGS_DIV_RE.sub(_sort_flags_div, xml)
return xml
def normalize_html(html):
return _FLAGS_DIV_RE.sub(_sort_flags_div, html)
return html
def golden_path(channel, kind):
@@ -154,7 +154,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 7&nbsp;&nbsp;</span><span class="reaction">❤ 1599&nbsp;&nbsp;</span><span class="reaction">👍 368&nbsp;&nbsp;</span><span class="reaction">🙏 132&nbsp;&nbsp;</span><span class="reaction">🔥 63&nbsp;&nbsp;</span><span class="reaction">🤡 12&nbsp;&nbsp;</span><span class="reaction">💅 6&nbsp;&nbsp;</span><span class="reaction">⚡ 4&nbsp;&nbsp;</span><span class="reaction">😢 3&nbsp;&nbsp;</span><span class="reaction">👎 2&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">63570 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">23/03/26, 12:55:02</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#387</span><br><a href="tg://resolve?domain=bladerunnerblues&amp;post=387">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/bladerunnerblues/387">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 no_image 🏷 hid_channel 🏷 merged 🏷 fwd 🏷 foreign_channel </div></div>
<br><div class="message-flags"> 🏷 no_image 🏷 fwd 🏷 link 🏷 mention 🏷 hid_channel 🏷 foreign_channel 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -419,7 +419,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 14&nbsp;&nbsp;</span><span class="reaction">❤ 1733&nbsp;&nbsp;</span><span class="reaction">🔥 461&nbsp;&nbsp;</span><span class="reaction">👍 210&nbsp;&nbsp;</span><span class="reaction">🤡 184&nbsp;&nbsp;</span><span class="reaction">😢 157&nbsp;&nbsp;</span><span class="reaction">🤬 53&nbsp;&nbsp;</span><span class="reaction">🤔 47&nbsp;&nbsp;</span><span class="reaction">💯 45&nbsp;&nbsp;</span><span class="reaction">🤣 12&nbsp;&nbsp;</span><span class="reaction">👎 6&nbsp;&nbsp;</span><span class="reaction">😎 6&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">92399 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">09/05/25, 09:44:44</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#362</span><br><a href="tg://resolve?domain=bladerunnerblues&amp;post=362">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/bladerunnerblues/362">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 merged 🏷 clownpoo </div></div>
<br><div class="message-flags"> 🏷 clownpoo 🏷 link 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -7,7 +7,7 @@
<docs>http://www.rssboard.org/rss-specification</docs>
<generator>python-feedgen</generator>
<language>ru</language>
<lastBuildDate>Mon, 06 Jul 2026 04:36:26 +0000</lastBuildDate>
<lastBuildDate>Mon, 06 Jul 2026 14:33:40 +0000</lastBuildDate>
<item>
<title>Дорогой экипаж!</title>
<link>https://t.me/bladerunnerblues/295</link>
@@ -1089,7 +1089,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 14&nbsp;&nbsp;</span><span class="reaction">❤ 1733&nbsp;&nbsp;</span><span class="reaction">🔥 461&nbsp;&nbsp;</span><span class="reaction">👍 210&nbsp;&nbsp;</span><span class="reaction">🤡 184&nbsp;&nbsp;</span><span class="reaction">😢 157&nbsp;&nbsp;</span><span class="reaction">🤬 53&nbsp;&nbsp;</span><span class="reaction">🤔 47&nbsp;&nbsp;</span><span class="reaction">💯 45&nbsp;&nbsp;</span><span class="reaction">🤣 12&nbsp;&nbsp;</span><span class="reaction">👎 6&nbsp;&nbsp;</span><span class="reaction">😎 6&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">92399 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">09/05/25, 09:44:44</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#362</span><br><a href="tg://resolve?domain=bladerunnerblues&amp;post=362">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/bladerunnerblues/362">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 merged 🏷 clownpoo </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 clownpoo 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/bladerunnerblues/362</guid>
<pubDate>Fri, 09 May 2025 09:44:44 +0000</pubDate>
</item>
@@ -1441,7 +1441,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 7&nbsp;&nbsp;</span><span class="reaction">❤ 1599&nbsp;&nbsp;</span><span class="reaction">👍 368&nbsp;&nbsp;</span><span class="reaction">🙏 132&nbsp;&nbsp;</span><span class="reaction">🔥 63&nbsp;&nbsp;</span><span class="reaction">🤡 12&nbsp;&nbsp;</span><span class="reaction">💅 6&nbsp;&nbsp;</span><span class="reaction">⚡ 4&nbsp;&nbsp;</span><span class="reaction">😢 3&nbsp;&nbsp;</span><span class="reaction">👎 2&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">63570 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">23/03/26, 12:55:02</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#387</span><br><a href="tg://resolve?domain=bladerunnerblues&amp;post=387">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/bladerunnerblues/387">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 no_image 🏷 hid_channel 🏷 merged 🏷 fwd 🏷 foreign_channel </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 fwd 🏷 link 🏷 mention 🏷 hid_channel 🏷 foreign_channel 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/bladerunnerblues/387</guid>
<pubDate>Mon, 23 Mar 2026 12:55:02 +0000</pubDate>
</item>
+9 -9
View File
@@ -63,7 +63,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">👏 11&nbsp;&nbsp;</span><span class="reaction">🤯 6&nbsp;&nbsp;</span><span class="reaction">👍 5&nbsp;&nbsp;</span><span class="reaction">🔥 2&nbsp;&nbsp;</span><span class="reaction">❓ 2&nbsp;&nbsp;</span><span class="reaction">🫡 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3233 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">04/05/26, 14:04:48</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1433</span><br><a href="tg://resolve?domain=embedoka&amp;post=1433">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1433">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -113,8 +113,8 @@
<br>
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 1&nbsp;&nbsp;</span><span class="reaction">🔥 13&nbsp;&nbsp;</span><span class="reaction">6&nbsp;&nbsp;</span><span class="reaction">😁 3&nbsp;&nbsp;</span><span class="reaction">❤ 1&nbsp;&nbsp;</span><span class="reaction">👍 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3365 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">10/04/26, 09:13:01</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&amp;post=1428">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1428">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>
<span class="reaction">⭐ 1&nbsp;&nbsp;</span><span class="reaction">🔥 13&nbsp;&nbsp;</span><span class="reaction">4&nbsp;&nbsp;</span><span class="reaction">😁 3&nbsp;&nbsp;</span><span class="reaction">❓ 2&nbsp;&nbsp;</span><span class="reaction">❤ 1&nbsp;&nbsp;</span><span class="reaction">👍 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3365 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">10/04/26, 09:13:01</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&amp;post=1428">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1428">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -306,7 +306,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 2&nbsp;&nbsp;</span><span class="reaction">👍 29&nbsp;&nbsp;</span><span class="reaction">✍ 4&nbsp;&nbsp;</span><span class="reaction">❤ 3&nbsp;&nbsp;</span><span class="reaction">🔥 1&nbsp;&nbsp;</span><span class="reaction">🤔 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">6052 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">02/09/25, 10:40:26</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1411</span><br><a href="tg://resolve?domain=embedoka&amp;post=1411">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1411">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -419,7 +419,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 1&nbsp;&nbsp;</span><span class="reaction">❤ 6&nbsp;&nbsp;</span><span class="reaction">🤣 4&nbsp;&nbsp;</span><span class="reaction">👍 3&nbsp;&nbsp;</span><span class="reaction">🤔 2&nbsp;&nbsp;</span><span class="reaction">✍ 1&nbsp;&nbsp;</span><span class="reaction">🔥 1&nbsp;&nbsp;</span><span class="reaction">🦄 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3573 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">14/08/25, 08:10:58</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1400</span><br><a href="tg://resolve?domain=embedoka&amp;post=1400">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1400">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-forward">--- Forwarded from <a href="https://t.me/embedoka">Embedded Doka (@embedoka)</a> ---</div>
@@ -768,7 +768,7 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">🔥 35&nbsp;&nbsp;</span><span class="reaction">👍 13&nbsp;&nbsp;</span><span class="reaction">👏 3&nbsp;&nbsp;</span><span class="reaction">🤔 3&nbsp;&nbsp;</span><span class="reaction">❤ 2&nbsp;&nbsp;</span><span class="reaction">✍ 1&nbsp;&nbsp;</span><span class="reaction">🤯 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">4838 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">22/05/25, 07:49:49</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1370</span><br><a href="tg://resolve?domain=embedoka&amp;post=1370">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1370">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -837,7 +837,7 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
<div class="message-poll">📊 Poll: Стоит ли выкладывать копии релевантных видео в канал (для участников, у которых ограничен доступ к ютьюб)? И вообще дублировать какой-либо контент в тг, доступ к которому может быть ограничен?<br>1. да<br>2. нет<br>3. всё равно<br>4. напишу в комментариях<br><br>→ Vote in Telegram 🔗<br></div>
</div><br></div>
<div class="message-footer"><br>
&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3822 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">05/05/25, 17:21:14</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&amp;post=1363">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1363">Open in Web</a>
<span class="views">3822 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">05/05/25, 17:21:14</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&amp;post=1363">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1363">Open in Web</a>
<br><div class="message-flags"> 🏷 no_image 🏷 poll </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
@@ -984,7 +984,7 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">🤯 11&nbsp;&nbsp;</span><span class="reaction">🤔 5&nbsp;&nbsp;</span><span class="reaction">🤷‍♂ 2&nbsp;&nbsp;</span><span class="reaction">👍 2&nbsp;&nbsp;</span><span class="reaction">🌚 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">4720 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">10/04/25, 08:45:12</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1350</span><br><a href="tg://resolve?domain=embedoka&amp;post=1350">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1350">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -1084,7 +1084,7 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 2&nbsp;&nbsp;</span><span class="reaction">❤ 41&nbsp;&nbsp;</span><span class="reaction">🔥 10&nbsp;&nbsp;</span><span class="reaction">😭 7&nbsp;&nbsp;</span><span class="reaction">🤪 4&nbsp;&nbsp;</span><span class="reaction">👍 3&nbsp;&nbsp;</span><span class="reaction">🎉 2&nbsp;&nbsp;</span><span class="reaction">🫡 1&nbsp;&nbsp;</span><span class="reaction">🦄 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">4253 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">06/03/25, 18:05:55</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1341</span><br><a href="tg://resolve?domain=embedoka&amp;post=1341">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1341">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<b>DIY rule #43</b>:<br><br>
+10 -10
View File
@@ -7,7 +7,7 @@
<docs>http://www.rssboard.org/rss-specification</docs>
<generator>python-feedgen</generator>
<language>ru</language>
<lastBuildDate>Mon, 06 Jul 2026 04:36:27 +0000</lastBuildDate>
<lastBuildDate>Mon, 06 Jul 2026 14:33:41 +0000</lastBuildDate>
<item>
<title>Интересно при чтении нехудожественных книг...</title>
<link>https://t.me/embedoka/1337</link>
@@ -114,7 +114,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 2&nbsp;&nbsp;</span><span class="reaction">❤ 41&nbsp;&nbsp;</span><span class="reaction">🔥 10&nbsp;&nbsp;</span><span class="reaction">😭 7&nbsp;&nbsp;</span><span class="reaction">🤪 4&nbsp;&nbsp;</span><span class="reaction">👍 3&nbsp;&nbsp;</span><span class="reaction">🎉 2&nbsp;&nbsp;</span><span class="reaction">🫡 1&nbsp;&nbsp;</span><span class="reaction">🦄 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">4253 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">06/03/25, 18:05:55</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1341</span><br><a href="tg://resolve?domain=embedoka&amp;post=1341">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1341">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/embedoka/1341</guid>
<pubDate>Thu, 06 Mar 2025 18:05:55 +0000</pubDate>
</item>
@@ -236,7 +236,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">🤯 11&nbsp;&nbsp;</span><span class="reaction">🤔 5&nbsp;&nbsp;</span><span class="reaction">🤷‍♂ 2&nbsp;&nbsp;</span><span class="reaction">👍 2&nbsp;&nbsp;</span><span class="reaction">🌚 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">4720 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">10/04/25, 08:45:12</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1350</span><br><a href="tg://resolve?domain=embedoka&amp;post=1350">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1350">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/embedoka/1350</guid>
<pubDate>Thu, 10 Apr 2025 08:45:12 +0000</pubDate>
</item>
@@ -434,7 +434,7 @@
<div class="message-poll">📊 Poll: Стоит ли выкладывать копии релевантных видео в канал (для участников, у которых ограничен доступ к ютьюб)? И вообще дублировать какой-либо контент в тг, доступ к которому может быть ограничен?<br>1. да<br>2. нет<br>3. всё равно<br>4. напишу в комментариях<br><br>→ Vote in Telegram 🔗<br></div>
</div><br></div>
<div class="message-footer"><br>
&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3822 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">05/05/25, 17:21:14</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&amp;post=1363">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1363">Open in Web</a>
<span class="views">3822 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">05/05/25, 17:21:14</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&amp;post=1363">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1363">Open in Web</a>
<br><div class="message-flags"> 🏷 no_image 🏷 poll </div></div>]]></description>
<guid isPermaLink="true">https://t.me/embedoka/1363</guid>
<pubDate>Mon, 05 May 2025 17:21:14 +0000</pubDate>
@@ -559,7 +559,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">🔥 35&nbsp;&nbsp;</span><span class="reaction">👍 13&nbsp;&nbsp;</span><span class="reaction">👏 3&nbsp;&nbsp;</span><span class="reaction">🤔 3&nbsp;&nbsp;</span><span class="reaction">❤ 2&nbsp;&nbsp;</span><span class="reaction">✍ 1&nbsp;&nbsp;</span><span class="reaction">🤯 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">4838 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">22/05/25, 07:49:49</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1370</span><br><a href="tg://resolve?domain=embedoka&amp;post=1370">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1370">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/embedoka/1370</guid>
<pubDate>Thu, 22 May 2025 07:49:49 +0000</pubDate>
</item>
@@ -1097,7 +1097,7 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 1&nbsp;&nbsp;</span><span class="reaction">❤ 6&nbsp;&nbsp;</span><span class="reaction">🤣 4&nbsp;&nbsp;</span><span class="reaction">👍 3&nbsp;&nbsp;</span><span class="reaction">🤔 2&nbsp;&nbsp;</span><span class="reaction">✍ 1&nbsp;&nbsp;</span><span class="reaction">🔥 1&nbsp;&nbsp;</span><span class="reaction">🦄 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3573 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">14/08/25, 08:10:58</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1400</span><br><a href="tg://resolve?domain=embedoka&amp;post=1400">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1400">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/embedoka/1400</guid>
<pubDate>Thu, 14 Aug 2025 08:10:58 +0000</pubDate>
</item>
@@ -1218,7 +1218,7 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 2&nbsp;&nbsp;</span><span class="reaction">👍 29&nbsp;&nbsp;</span><span class="reaction">✍ 4&nbsp;&nbsp;</span><span class="reaction">❤ 3&nbsp;&nbsp;</span><span class="reaction">🔥 1&nbsp;&nbsp;</span><span class="reaction">🤔 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">6052 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">02/09/25, 10:40:26</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1411</span><br><a href="tg://resolve?domain=embedoka&amp;post=1411">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1411">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/embedoka/1411</guid>
<pubDate>Tue, 02 Sep 2025 10:40:26 +0000</pubDate>
</item>
@@ -1506,8 +1506,8 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
<br>
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">⭐ 1&nbsp;&nbsp;</span><span class="reaction">🔥 13&nbsp;&nbsp;</span><span class="reaction">❓ 6&nbsp;&nbsp;</span><span class="reaction">😁 3&nbsp;&nbsp;</span><span class="reaction">❤ 1&nbsp;&nbsp;</span><span class="reaction">👍 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3365 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">10/04/26, 09:13:01</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&amp;post=1428">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1428">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>]]></content:encoded>
<span class="reaction">⭐ 1&nbsp;&nbsp;</span><span class="reaction">🔥 13&nbsp;&nbsp;</span><span class="reaction">❓ 4&nbsp;&nbsp;</span><span class="reaction">😁 3&nbsp;&nbsp;</span><span class="reaction">❓ 2&nbsp;&nbsp;</span><span class="reaction">❤ 1&nbsp;&nbsp;</span><span class="reaction">👍 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3365 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">10/04/26, 09:13:01</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&amp;post=1428">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1428">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/embedoka/1428</guid>
<pubDate>Fri, 10 Apr 2026 09:13:01 +0000</pubDate>
</item>
@@ -1569,7 +1569,7 @@ The SM9 module boasts <b>hardware compatibility</b> with NVIDIA Jetson series mo
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">👏 11&nbsp;&nbsp;</span><span class="reaction">🤯 6&nbsp;&nbsp;</span><span class="reaction">👍 5&nbsp;&nbsp;</span><span class="reaction">🔥 2&nbsp;&nbsp;</span><span class="reaction">❓ 2&nbsp;&nbsp;</span><span class="reaction">🫡 1&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">3233 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">04/05/26, 14:04:48</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#1433</span><br><a href="tg://resolve?domain=embedoka&amp;post=1433">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/embedoka/1433">Open in Web</a>
<br><div class="message-flags"> 🏷 mention 🏷 link 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/embedoka/1433</guid>
<pubDate>Mon, 04 May 2026 14:04:48 +0000</pubDate>
</item>
File diff suppressed because one or more lines are too long
+10 -10
View File
@@ -7,7 +7,7 @@
<docs>http://www.rssboard.org/rss-specification</docs>
<generator>python-feedgen</generator>
<language>ru</language>
<lastBuildDate>Mon, 06 Jul 2026 04:36:28 +0000</lastBuildDate>
<lastBuildDate>Mon, 06 Jul 2026 14:33:42 +0000</lastBuildDate>
<item>
<title>Мы уже давно ушли из той эпохи веб-дизайна, где все...</title>
<link>https://t.me/meow_design/2158</link>
@@ -325,7 +325,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1187 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">22/12/25, 15:22:09</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2183</span><br><a href="tg://resolve?domain=meow_design&amp;post=2183">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2183">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 no_image 🏷 merged 🏷 fwd 🏷 foreign_channel </div></div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 fwd 🏷 no_image 🏷 link 🏷 mention 🏷 foreign_channel 🏷 merged </div></div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2183</guid>
<pubDate>Mon, 22 Dec 2025 15:22:09 +0000</pubDate>
</item>
@@ -344,7 +344,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1279 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">22/12/25, 15:22:40</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2185</span><br><a href="tg://resolve?domain=meow_design&amp;post=2185">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2185">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2185</guid>
<pubDate>Mon, 22 Dec 2025 15:22:40 +0000</pubDate>
</item>
@@ -447,7 +447,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1605 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">02/01/26, 15:48:45</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2195</span><br><a href="tg://resolve?domain=meow_design&amp;post=2195">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2195">Open in Web</a>
<br><div class="message-flags"> 🏷 poll 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 poll 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2195</guid>
<pubDate>Fri, 02 Jan 2026 15:48:45 +0000</pubDate>
</item>
@@ -620,7 +620,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1738 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">05/02/26, 18:13:58</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2210</span><br><a href="tg://resolve?domain=meow_design&amp;post=2210">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2210">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2210</guid>
<pubDate>Thu, 05 Feb 2026 18:13:58 +0000</pubDate>
</item>
@@ -789,7 +789,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1051 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">19/04/26, 11:55:56</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2222</span><br><a href="tg://resolve?domain=meow_design&amp;post=2222">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2222">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2222</guid>
<pubDate>Sun, 19 Apr 2026 11:55:56 +0000</pubDate>
</item>
@@ -839,7 +839,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1005 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">20/04/26, 19:48:52</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2233</span><br><a href="tg://resolve?domain=meow_design&amp;post=2233">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2233">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2233</guid>
<pubDate>Mon, 20 Apr 2026 19:48:52 +0000</pubDate>
</item>
@@ -972,7 +972,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1337 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">14/05/26, 13:10:50</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2241</span><br><a href="tg://resolve?domain=meow_design&amp;post=2241">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2241">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2241</guid>
<pubDate>Thu, 14 May 2026 13:10:50 +0000</pubDate>
</item>
@@ -1042,7 +1042,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">1248 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">03/06/26, 22:18:58</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2257</span><br><a href="tg://resolve?domain=meow_design&amp;post=2257">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2257">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2257</guid>
<pubDate>Wed, 03 Jun 2026 22:18:58 +0000</pubDate>
</item>
@@ -1066,7 +1066,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="views">745 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">24/06/26, 02:29:30</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#2260</span><br><a href="tg://resolve?domain=meow_design&amp;post=2260">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/meow_design/2260">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 no_image 🏷 link 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/meow_design/2260</guid>
<pubDate>Wed, 24 Jun 2026 02:29:30 +0000</pubDate>
</item>
@@ -235,7 +235,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">❤‍🔥 64&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">24432 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">17/04/23, 17:13:03</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#3561</span><br><a href="tg://resolve?domain=theyforcedme&amp;post=3561">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/theyforcedme/3561">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 audio 🏷 no_image 🏷 merged </div></div>
<br><div class="message-flags"> 🏷 audio 🏷 link 🏷 no_image 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -344,7 +344,7 @@ growing up is overrated!!<br><br>my girlfriends prepared an easter egg hunt for
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">❤‍🔥 74&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">11150 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">09/04/23, 14:06:27</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#3549</span><br><a href="tg://resolve?domain=theyforcedme&amp;post=3549">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/theyforcedme/3549">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 no_image 🏷 merged 🏷 fwd 🏷 foreign_channel </div></div>
<br><div class="message-flags"> 🏷 fwd 🏷 link 🏷 mention 🏷 foreign_channel 🏷 no_image 🏷 merged </div></div>
<hr class="post-divider">
<div class="message-body"><div class="post">
<div class="message-media">
@@ -1052,4 +1052,4 @@ That is completely untrue. This is not how I close a bag of chips.
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">❤‍🔥 138&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">26921 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">16/01/23, 20:46:53</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#3483</span><br><a href="tg://resolve?domain=theyforcedme&amp;post=3483">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/theyforcedme/3483">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 merged 🏷 video </div></div>
<br><div class="message-flags"> 🏷 link 🏷 video 🏷 merged </div></div>
+4 -4
View File
@@ -7,7 +7,7 @@
<docs>http://www.rssboard.org/rss-specification</docs>
<generator>python-feedgen</generator>
<language>ru</language>
<lastBuildDate>Mon, 06 Jul 2026 04:36:28 +0000</lastBuildDate>
<lastBuildDate>Mon, 06 Jul 2026 14:33:43 +0000</lastBuildDate>
<item>
<title>Читаем кошку</title>
<link>https://t.me/theyforcedme/3483</link>
@@ -29,7 +29,7 @@
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">❤‍🔥 138&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">26921 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">16/01/23, 20:46:53</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#3483</span><br><a href="tg://resolve?domain=theyforcedme&amp;post=3483">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/theyforcedme/3483">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 merged 🏷 video </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 link 🏷 video 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/theyforcedme/3483</guid>
<pubDate>Mon, 16 Jan 2023 20:46:53 +0000</pubDate>
</item>
@@ -1111,7 +1111,7 @@ growing up is overrated!!<br><br>my girlfriends prepared an easter egg hunt for
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">❤‍🔥 74&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">11150 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">09/04/23, 14:06:27</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#3549</span><br><a href="tg://resolve?domain=theyforcedme&amp;post=3549">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/theyforcedme/3549">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 mention 🏷 no_image 🏷 merged 🏷 fwd 🏷 foreign_channel </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 fwd 🏷 link 🏷 mention 🏷 foreign_channel 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/theyforcedme/3549</guid>
<pubDate>Sun, 09 Apr 2023 14:06:27 +0000</pubDate>
</item>
@@ -1245,7 +1245,7 @@ Grammarly, ты очень хорошо корректируешь имейлы,
</div><br></div>
<div class="message-footer"><br>
<span class="reaction">❤‍🔥 64&nbsp;&nbsp;</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="views">24432 👁</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="date">17/04/23, 17:13:03</span>&nbsp;&nbsp;|&nbsp;&nbsp;<span class="message-id">#3561</span><br><a href="tg://resolve?domain=theyforcedme&amp;post=3561">Open in Telegram</a>&nbsp;|&nbsp;<a href="https://t.me/theyforcedme/3561">Open in Web</a>
<br><div class="message-flags"> 🏷 link 🏷 audio 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<br><div class="message-flags"> 🏷 audio 🏷 link 🏷 no_image 🏷 merged </div></div>]]></content:encoded>
<guid isPermaLink="true">https://t.me/theyforcedme/3561</guid>
<pubDate>Mon, 17 Apr 2023 17:13:03 +0000</pubDate>
</item>
+12 -11
View File
@@ -3,9 +3,10 @@
"""Stage-0 golden oracle (render-pipeline refactor epic, issue #27/#34).
Regenerates the RSS + HTML feeds for the frozen recorded corpus and asserts
byte-equality against the committed goldens after the spec's declared normalizations
(strip volatile <lastBuildDate>, sort the hash-ordered merged-flags div). Any other
byte change = a render regression, and this test must catch it.
byte-equality against the committed goldens after the spec's declared normalization
(strip volatile <lastBuildDate>). Any other byte change = a render regression, and
this test must catch it. (The stage-0 merged-flags sort normalization was removed in
stage 2: flag order is now deterministic first-seen order — §3.8.)
Guardrails: this stage only ADDS a loader + goldens + this test. No render/pipeline
production code is touched; the goldens freeze CURRENT behavior including known bugs
@@ -60,14 +61,14 @@ def test_all_goldens_present_and_nonempty():
assert os.path.getsize(path) > 0, f"empty golden: {path}"
def test_normalization_sorts_merged_flags():
"""Guard the load-bearing flag-sort normalization itself: merged-post flags are emitted
as list(set(...)) (hash-ordered), so the normalizer must canonicalize their order."""
unsorted = '<div class="message-flags"> 🏷 video 🏷 fwd 🏷 link </div>'
resorted = '<div class="message-flags"> 🏷 link 🏷 fwd 🏷 video </div>'
assert gr.normalize_html(unsorted) == gr.normalize_html(resorted)
# ...and it is not a no-op that would let a real reordering slip through undetected.
assert gr.normalize_html(unsorted) == '<div class="message-flags"> 🏷 fwd 🏷 link 🏷 video </div>'
def test_normalization_preserves_flag_order():
"""Stage 2 (§3.8): merged-post flags are now emitted in deterministic first-seen
order, so normalize_html no longer reorders the flags div — a real flag reordering
must reach the byte comparison instead of being masked."""
a = '<div class="message-flags"> 🏷 video 🏷 fwd 🏷 link </div>'
b = '<div class="message-flags"> 🏷 link 🏷 fwd 🏷 video </div>'
assert gr.normalize_html(a) != gr.normalize_html(b)
assert gr.normalize_html(a) == a
def test_normalization_strips_lastbuilddate():
+188
View File
@@ -0,0 +1,188 @@
# flake8: noqa
# pylint: disable=missing-function-docstring, redefined-outer-name, line-too-long
# pylint: disable=protected-access, wrong-import-position, import-outside-toplevel
"""Stage-2 footer tests (render-pipeline refactor epic, issue #29/#34).
Stage 2 removed rss_generator.processed_message_to_tg_message and renders the merged
footer DIRECTLY from the real main Message. These tests lock the registry §3 items that
become visible as a consequence:
§3.6 custom-emoji reactions in the merged footer get a separate "❓ N" span each
(no more aggregation into one "❓ N"), matching single posts.
§3.7 the merged footer date comes from the naive-local date of the real Message,
not a UTC mock — verified with a NON-UTC TZ where the delta is visible (the
TZ=UTC golden cannot see it).
§3.15 an empty reactions object no longer emits a leading footer separator (single
posts too).
Messages are built with the shared SimpleNamespace helper (naive dates where relevant,
as kurigram emits on prod).
"""
import os
import time
from datetime import datetime
from types import SimpleNamespace
import pytest
from pyrogram.enums import MessageMediaType
from post_parser import PostParser
from rss_generator import _render_messages_groups
from tests.test_stage4_eventloop import make_message
def _custom_reaction(count, custom_emoji_id):
# No `.emoji` attribute -> _reactions_views_links falls to the custom-emoji "❓" branch.
return SimpleNamespace(count=count, custom_emoji_id=custom_emoji_id)
def _normal_reaction(count, emoji):
return SimpleNamespace(count=count, emoji=emoji)
def _reactions(*items):
return SimpleNamespace(reactions=list(items))
def _footer_of(post):
"""Extract the <div class="message-footer">…</div> inner html from a rendered post."""
html = post["html"]
marker = '<div class="message-footer">'
start = html.index(marker) + len(marker)
return html[start:]
# --------------------------------------------------------------------------- #
# §3.6 — custom-emoji reactions render as one span each, in the merged footer.
# --------------------------------------------------------------------------- #
def test_3_6_two_custom_emoji_render_as_separate_spans():
"""Single-post baseline: _reactions_views_links (the function the merged footer now
uses on the real Message) emits one '❓ N' span per custom emoji, never aggregated."""
parser = PostParser(SimpleNamespace())
msg = make_message(70, text="hi")
msg.reactions = _reactions(_custom_reaction(4, 111), _custom_reaction(2, 222))
html = parser._reactions_views_links(msg)
assert html.count('<span class="reaction">❓ 4') == 1
assert html.count('<span class="reaction">❓ 2') == 1
# NOT aggregated into a single "❓ 6".
assert "❓ 6" not in html
def test_3_6_merged_footer_keeps_custom_spans_separate():
"""Merged footer, rendered from the real main Message, keeps a span per custom emoji.
The old dict round-trip (_extract_reactions) collapsed both customs into one '❓ 6'."""
parser = PostParser(SimpleNamespace())
main = make_message(80, text="main text")
main.media_group_id = "mg_36"
main.reactions = _reactions(
_normal_reaction(13, "🔥"),
_custom_reaction(4, 111),
_custom_reaction(2, 222),
)
other = make_message(81, text="second part")
other.media_group_id = "mg_36"
posts = _render_messages_groups([[main, other]], parser)
assert len(posts) == 1
footer = _footer_of(posts[0])
assert "merged" in posts[0]["flags"]
assert footer.count('<span class="reaction">❓ 4') == 1
assert footer.count('<span class="reaction">❓ 2') == 1
assert "❓ 6" not in footer # would appear if the customs were still aggregated
# --------------------------------------------------------------------------- #
# §3.15 — an empty reactions object emits no leading footer separator.
# --------------------------------------------------------------------------- #
_SEP = "&nbsp;&nbsp;|&nbsp;&nbsp;"
def test_3_15_empty_reactions_no_leading_separator():
"""A reactions object with an empty list must not produce a leading '…|…' separator.
Affects single posts (this call site) as well as merged ones."""
parser = PostParser(SimpleNamespace())
msg = make_message(90, text="hi")
msg.reactions = _reactions() # object present, no reactions inside
html = parser._reactions_views_links(msg)
# First visible element must be the views span, not the separator.
assert html.startswith('<span class="views">')
assert not html.startswith(_SEP)
def test_3_15_empty_reactions_single_post_render():
"""Same via the real render path: the footer's first line starts with views, no
leading separator injected by the empty reactions object."""
parser = PostParser(SimpleNamespace())
msg = make_message(91, text="hi")
msg.reactions = _reactions()
posts = _render_messages_groups([[msg]], parser)
assert len(posts) == 1
footer = _footer_of(posts[0])
assert f'<br>\n{_SEP}<span class="views">' not in footer
assert '<br>\n<span class="views">' in footer
def test_3_15_present_reactions_still_render():
"""Control: a non-empty reactions object still renders its spans (the §3.15 guard is
not a blanket drop of the reactions line)."""
parser = PostParser(SimpleNamespace())
msg = make_message(92, text="hi")
msg.reactions = _reactions(_normal_reaction(5, "🔥"))
html = parser._reactions_views_links(msg)
assert html.startswith('<span class="reaction">🔥 5')
# --------------------------------------------------------------------------- #
# §3.7 — merged footer date == single-post date under a NON-UTC TZ.
# --------------------------------------------------------------------------- #
@pytest.fixture
def moscow_tz():
"""Pin a non-UTC TZ for the duration of the test, then restore the conftest UTC pin.
Without restoring, a leaked non-UTC TZ would corrupt the UTC-pinned golden tests."""
os.environ["TZ"] = "Europe/Moscow"
time.tzset()
try:
yield
finally:
os.environ["TZ"] = "UTC"
time.tzset()
def _date_span(footer):
marker = '<span class="date">'
start = footer.index(marker) + len(marker)
end = footer.index("</span>", start)
return footer[start:end]
def test_3_7_merged_footer_date_matches_single_post_non_utc(moscow_tz):
"""On a TZ≠UTC server the merged footer date must equal the single-post date for the
same message. Before stage 2 the merged path built a UTC mock date (from the naive
date's timestamp) → a visible offset; now both render the real naive-local date."""
parser = PostParser(SimpleNamespace())
# NAIVE date, as kurigram emits on prod. Under Europe/Moscow the old UTC mock would
# have shifted this by the TZ offset.
naive_date = datetime(2026, 5, 5, 17, 21, 14)
def _fresh_main():
m = make_message(100, text="main text", date=naive_date)
return m
# Single post.
single_posts = _render_messages_groups([[_fresh_main()]], parser)
assert len(single_posts) == 1
single_date = _date_span(_footer_of(single_posts[0]))
# Merged group with the same main message + one extra member.
main = _fresh_main()
main.media_group_id = "mg_37"
other = make_message(101, text="second part", date=naive_date)
other.media_group_id = "mg_37"
merged_posts = _render_messages_groups([[main, other]], parser)
assert len(merged_posts) == 1
assert "merged" in merged_posts[0]["flags"]
merged_date = _date_span(_footer_of(merged_posts[0]))
assert merged_date == single_date
# And it is the naive wall-clock time, not a UTC-shifted one.
assert merged_date == "05/05/26, 17:21:14"