Merge pull request 'refactor(render): этап 2 — футер merged-групп из настоящего Message (#29)' (#38) from refactor/render-stage2-footer into main
Docker Image CI / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
This commit was merged in pull request #38.
This commit is contained in:
+6
-1
@@ -1055,7 +1055,12 @@ class PostParser:
|
||||
else: emoji = "❓" # Default for unknown cases
|
||||
reactions_html += f'<span class="reaction">{emoji} {reaction.count} </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
|
||||
# ' | ' 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
@@ -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
@@ -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 </span><span class="reaction">❤ 1599 </span><span class="reaction">👍 368 </span><span class="reaction">🙏 132 </span><span class="reaction">🔥 63 </span><span class="reaction">🤡 12 </span><span class="reaction">💅 6 </span><span class="reaction">⚡ 4 </span><span class="reaction">😢 3 </span><span class="reaction">👎 2 </span> | <span class="views">63570 👁</span> | <span class="date">23/03/26, 12:55:02</span> | <span class="message-id">#387</span><br><a href="tg://resolve?domain=bladerunnerblues&post=387">Open in Telegram</a> | <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 </span><span class="reaction">❤ 1733 </span><span class="reaction">🔥 461 </span><span class="reaction">👍 210 </span><span class="reaction">🤡 184 </span><span class="reaction">😢 157 </span><span class="reaction">🤬 53 </span><span class="reaction">🤔 47 </span><span class="reaction">💯 45 </span><span class="reaction">🤣 12 </span><span class="reaction">👎 6 </span><span class="reaction">😎 6 </span> | <span class="views">92399 👁</span> | <span class="date">09/05/25, 09:44:44</span> | <span class="message-id">#362</span><br><a href="tg://resolve?domain=bladerunnerblues&post=362">Open in Telegram</a> | <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 </span><span class="reaction">❤ 1733 </span><span class="reaction">🔥 461 </span><span class="reaction">👍 210 </span><span class="reaction">🤡 184 </span><span class="reaction">😢 157 </span><span class="reaction">🤬 53 </span><span class="reaction">🤔 47 </span><span class="reaction">💯 45 </span><span class="reaction">🤣 12 </span><span class="reaction">👎 6 </span><span class="reaction">😎 6 </span> | <span class="views">92399 👁</span> | <span class="date">09/05/25, 09:44:44</span> | <span class="message-id">#362</span><br><a href="tg://resolve?domain=bladerunnerblues&post=362">Open in Telegram</a> | <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 </span><span class="reaction">❤ 1599 </span><span class="reaction">👍 368 </span><span class="reaction">🙏 132 </span><span class="reaction">🔥 63 </span><span class="reaction">🤡 12 </span><span class="reaction">💅 6 </span><span class="reaction">⚡ 4 </span><span class="reaction">😢 3 </span><span class="reaction">👎 2 </span> | <span class="views">63570 👁</span> | <span class="date">23/03/26, 12:55:02</span> | <span class="message-id">#387</span><br><a href="tg://resolve?domain=bladerunnerblues&post=387">Open in Telegram</a> | <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>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</div><br></div>
|
||||
<div class="message-footer"><br>
|
||||
<span class="reaction">👏 11 </span><span class="reaction">🤯 6 </span><span class="reaction">👍 5 </span><span class="reaction">🔥 2 </span><span class="reaction">❓ 2 </span><span class="reaction">🫡 1 </span> | <span class="views">3233 👁</span> | <span class="date">04/05/26, 14:04:48</span> | <span class="message-id">#1433</span><br><a href="tg://resolve?domain=embedoka&post=1433">Open in Telegram</a> | <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 </span><span class="reaction">🔥 13 </span><span class="reaction">❓ 6 </span><span class="reaction">😁 3 </span><span class="reaction">❤ 1 </span><span class="reaction">👍 1 </span> | <span class="views">3365 👁</span> | <span class="date">10/04/26, 09:13:01</span> | <span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&post=1428">Open in Telegram</a> | <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 </span><span class="reaction">🔥 13 </span><span class="reaction">❓ 4 </span><span class="reaction">😁 3 </span><span class="reaction">❓ 2 </span><span class="reaction">❤ 1 </span><span class="reaction">👍 1 </span> | <span class="views">3365 👁</span> | <span class="date">10/04/26, 09:13:01</span> | <span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&post=1428">Open in Telegram</a> | <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 </span><span class="reaction">👍 29 </span><span class="reaction">✍ 4 </span><span class="reaction">❤ 3 </span><span class="reaction">🔥 1 </span><span class="reaction">🤔 1 </span> | <span class="views">6052 👁</span> | <span class="date">02/09/25, 10:40:26</span> | <span class="message-id">#1411</span><br><a href="tg://resolve?domain=embedoka&post=1411">Open in Telegram</a> | <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 </span><span class="reaction">❤ 6 </span><span class="reaction">🤣 4 </span><span class="reaction">👍 3 </span><span class="reaction">🤔 2 </span><span class="reaction">✍ 1 </span><span class="reaction">🔥 1 </span><span class="reaction">🦄 1 </span> | <span class="views">3573 👁</span> | <span class="date">14/08/25, 08:10:58</span> | <span class="message-id">#1400</span><br><a href="tg://resolve?domain=embedoka&post=1400">Open in Telegram</a> | <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 </span><span class="reaction">👍 13 </span><span class="reaction">👏 3 </span><span class="reaction">🤔 3 </span><span class="reaction">❤ 2 </span><span class="reaction">✍ 1 </span><span class="reaction">🤯 1 </span> | <span class="views">4838 👁</span> | <span class="date">22/05/25, 07:49:49</span> | <span class="message-id">#1370</span><br><a href="tg://resolve?domain=embedoka&post=1370">Open in Telegram</a> | <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>
|
||||
| <span class="views">3822 👁</span> | <span class="date">05/05/25, 17:21:14</span> | <span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&post=1363">Open in Telegram</a> | <a href="https://t.me/embedoka/1363">Open in Web</a>
|
||||
<span class="views">3822 👁</span> | <span class="date">05/05/25, 17:21:14</span> | <span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&post=1363">Open in Telegram</a> | <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 </span><span class="reaction">🤔 5 </span><span class="reaction">🤷♂ 2 </span><span class="reaction">👍 2 </span><span class="reaction">🌚 1 </span> | <span class="views">4720 👁</span> | <span class="date">10/04/25, 08:45:12</span> | <span class="message-id">#1350</span><br><a href="tg://resolve?domain=embedoka&post=1350">Open in Telegram</a> | <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 </span><span class="reaction">❤ 41 </span><span class="reaction">🔥 10 </span><span class="reaction">😭 7 </span><span class="reaction">🤪 4 </span><span class="reaction">👍 3 </span><span class="reaction">🎉 2 </span><span class="reaction">🫡 1 </span><span class="reaction">🦄 1 </span> | <span class="views">4253 👁</span> | <span class="date">06/03/25, 18:05:55</span> | <span class="message-id">#1341</span><br><a href="tg://resolve?domain=embedoka&post=1341">Open in Telegram</a> | <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>
|
||||
|
||||
@@ -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 </span><span class="reaction">❤ 41 </span><span class="reaction">🔥 10 </span><span class="reaction">😭 7 </span><span class="reaction">🤪 4 </span><span class="reaction">👍 3 </span><span class="reaction">🎉 2 </span><span class="reaction">🫡 1 </span><span class="reaction">🦄 1 </span> | <span class="views">4253 👁</span> | <span class="date">06/03/25, 18:05:55</span> | <span class="message-id">#1341</span><br><a href="tg://resolve?domain=embedoka&post=1341">Open in Telegram</a> | <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 </span><span class="reaction">🤔 5 </span><span class="reaction">🤷♂ 2 </span><span class="reaction">👍 2 </span><span class="reaction">🌚 1 </span> | <span class="views">4720 👁</span> | <span class="date">10/04/25, 08:45:12</span> | <span class="message-id">#1350</span><br><a href="tg://resolve?domain=embedoka&post=1350">Open in Telegram</a> | <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>
|
||||
| <span class="views">3822 👁</span> | <span class="date">05/05/25, 17:21:14</span> | <span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&post=1363">Open in Telegram</a> | <a href="https://t.me/embedoka/1363">Open in Web</a>
|
||||
<span class="views">3822 👁</span> | <span class="date">05/05/25, 17:21:14</span> | <span class="message-id">#1363</span><br><a href="tg://resolve?domain=embedoka&post=1363">Open in Telegram</a> | <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 </span><span class="reaction">👍 13 </span><span class="reaction">👏 3 </span><span class="reaction">🤔 3 </span><span class="reaction">❤ 2 </span><span class="reaction">✍ 1 </span><span class="reaction">🤯 1 </span> | <span class="views">4838 👁</span> | <span class="date">22/05/25, 07:49:49</span> | <span class="message-id">#1370</span><br><a href="tg://resolve?domain=embedoka&post=1370">Open in Telegram</a> | <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 </span><span class="reaction">❤ 6 </span><span class="reaction">🤣 4 </span><span class="reaction">👍 3 </span><span class="reaction">🤔 2 </span><span class="reaction">✍ 1 </span><span class="reaction">🔥 1 </span><span class="reaction">🦄 1 </span> | <span class="views">3573 👁</span> | <span class="date">14/08/25, 08:10:58</span> | <span class="message-id">#1400</span><br><a href="tg://resolve?domain=embedoka&post=1400">Open in Telegram</a> | <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 </span><span class="reaction">👍 29 </span><span class="reaction">✍ 4 </span><span class="reaction">❤ 3 </span><span class="reaction">🔥 1 </span><span class="reaction">🤔 1 </span> | <span class="views">6052 👁</span> | <span class="date">02/09/25, 10:40:26</span> | <span class="message-id">#1411</span><br><a href="tg://resolve?domain=embedoka&post=1411">Open in Telegram</a> | <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 </span><span class="reaction">🔥 13 </span><span class="reaction">❓ 6 </span><span class="reaction">😁 3 </span><span class="reaction">❤ 1 </span><span class="reaction">👍 1 </span> | <span class="views">3365 👁</span> | <span class="date">10/04/26, 09:13:01</span> | <span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&post=1428">Open in Telegram</a> | <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 </span><span class="reaction">🔥 13 </span><span class="reaction">❓ 4 </span><span class="reaction">😁 3 </span><span class="reaction">❓ 2 </span><span class="reaction">❤ 1 </span><span class="reaction">👍 1 </span> | <span class="views">3365 👁</span> | <span class="date">10/04/26, 09:13:01</span> | <span class="message-id">#1428</span><br><a href="tg://resolve?domain=embedoka&post=1428">Open in Telegram</a> | <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 </span><span class="reaction">🤯 6 </span><span class="reaction">👍 5 </span><span class="reaction">🔥 2 </span><span class="reaction">❓ 2 </span><span class="reaction">🫡 1 </span> | <span class="views">3233 👁</span> | <span class="date">04/05/26, 14:04:48</span> | <span class="message-id">#1433</span><br><a href="tg://resolve?domain=embedoka&post=1433">Open in Telegram</a> | <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
@@ -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> | <span class="date">22/12/25, 15:22:09</span> | <span class="message-id">#2183</span><br><a href="tg://resolve?domain=meow_design&post=2183">Open in Telegram</a> | <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> | <span class="date">22/12/25, 15:22:40</span> | <span class="message-id">#2185</span><br><a href="tg://resolve?domain=meow_design&post=2185">Open in Telegram</a> | <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> | <span class="date">02/01/26, 15:48:45</span> | <span class="message-id">#2195</span><br><a href="tg://resolve?domain=meow_design&post=2195">Open in Telegram</a> | <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> | <span class="date">05/02/26, 18:13:58</span> | <span class="message-id">#2210</span><br><a href="tg://resolve?domain=meow_design&post=2210">Open in Telegram</a> | <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> | <span class="date">19/04/26, 11:55:56</span> | <span class="message-id">#2222</span><br><a href="tg://resolve?domain=meow_design&post=2222">Open in Telegram</a> | <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> | <span class="date">20/04/26, 19:48:52</span> | <span class="message-id">#2233</span><br><a href="tg://resolve?domain=meow_design&post=2233">Open in Telegram</a> | <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> | <span class="date">14/05/26, 13:10:50</span> | <span class="message-id">#2241</span><br><a href="tg://resolve?domain=meow_design&post=2241">Open in Telegram</a> | <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> | <span class="date">03/06/26, 22:18:58</span> | <span class="message-id">#2257</span><br><a href="tg://resolve?domain=meow_design&post=2257">Open in Telegram</a> | <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> | <span class="date">24/06/26, 02:29:30</span> | <span class="message-id">#2260</span><br><a href="tg://resolve?domain=meow_design&post=2260">Open in Telegram</a> | <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 </span> | <span class="views">24432 👁</span> | <span class="date">17/04/23, 17:13:03</span> | <span class="message-id">#3561</span><br><a href="tg://resolve?domain=theyforcedme&post=3561">Open in Telegram</a> | <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 </span> | <span class="views">11150 👁</span> | <span class="date">09/04/23, 14:06:27</span> | <span class="message-id">#3549</span><br><a href="tg://resolve?domain=theyforcedme&post=3549">Open in Telegram</a> | <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 </span> | <span class="views">26921 👁</span> | <span class="date">16/01/23, 20:46:53</span> | <span class="message-id">#3483</span><br><a href="tg://resolve?domain=theyforcedme&post=3483">Open in Telegram</a> | <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>
|
||||
@@ -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 </span> | <span class="views">26921 👁</span> | <span class="date">16/01/23, 20:46:53</span> | <span class="message-id">#3483</span><br><a href="tg://resolve?domain=theyforcedme&post=3483">Open in Telegram</a> | <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 </span> | <span class="views">11150 👁</span> | <span class="date">09/04/23, 14:06:27</span> | <span class="message-id">#3549</span><br><a href="tg://resolve?domain=theyforcedme&post=3549">Open in Telegram</a> | <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 </span> | <span class="views">24432 👁</span> | <span class="date">17/04/23, 17:13:03</span> | <span class="message-id">#3561</span><br><a href="tg://resolve?domain=theyforcedme&post=3561">Open in Telegram</a> | <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
@@ -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():
|
||||
|
||||
@@ -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 = " | "
|
||||
|
||||
|
||||
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"
|
||||
Reference in New Issue
Block a user