Rename to Extended Media Forwards and forward rich message media
Publish plugin / publish (push) Successful in 5s

Rename the plugin (id extended_media_forwards) and add the Forward button
to media opened from rich messages, both single media and carousels.

Rich message media opens PhotoViewer through ChatActivity's
ChatArticlePageBlocksAdapter, whose parent object is the MessageObject
rather than a WebPage, so the article-only check hid the button. The
re-sent media links back to the message when it lives in a channel or
supergroup; elsewhere it is sent without a caption. FileLoader now gets
the parent object so expired file references can be refreshed.

Instant View carousels already worked: slideshow items are flattened
into the article's photo list. Findings are in docs/media-viewer-notes.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-07 00:51:58 +02:00
1 parent 05c5212a8a
commit 46503f24a0
9 files changed
+317 -36

No files matched your search

+101 -1
View File
@@ -116,6 +116,42 @@ class FakeWebPage:
self.display_url = display_url
class FakeRichMessage:
"""Stand-in for TL_iv.RichMessage."""
class FakeMessage:
def __init__(self, rich_message=None):
self.rich_message = rich_message
class FakeMessageObject:
def __init__(self, msg_id, dialog_id, rich_message=None):
self.messageOwner = FakeMessage(rich_message)
self._id = msg_id
self._dialog_id = dialog_id
def getId(self):
return self._id
def getDialogId(self):
return self._dialog_id
class FakeChat:
def __init__(self, chat_id, channel, username=None):
self.id = chat_id
self.channel = channel
self.username = username
FAKE_CHATS = {
100: FakeChat(100, channel=True, username="publicchan"),
200: FakeChat(200, channel=True),
300: FakeChat(300, channel=False),
}
def install_fakes() -> None:
base_plugin_mod = types.ModuleType("base_plugin")
base_plugin_mod.BasePlugin = BasePlugin
@@ -158,6 +194,22 @@ def install_fakes() -> None:
),
)
tgnet_mod.TLRPC = tlrpc
tgnet_mod.__path__ = []
tgnet_tl_mod = types.ModuleType("org.telegram.tgnet.tl")
tgnet_tl_mod.TL_iv = types.SimpleNamespace(RichMessage=FakeRichMessage)
tgnet_mod.tl = tgnet_tl_mod
sys.modules["org.telegram.tgnet.tl"] = tgnet_tl_mod
messenger_mod = types.ModuleType("org.telegram.messenger")
messenger_mod.MessageObject = FakeMessageObject
messenger_mod.ChatObject = types.SimpleNamespace(
isChannel=lambda chat: chat.channel,
getPublicUsername=lambda chat: chat.username,
)
messenger_mod.MessagesController = types.SimpleNamespace(
getInstance=lambda account: types.SimpleNamespace(getChat=FAKE_CHATS.get)
)
sys.modules["org.telegram.messenger"] = messenger_mod
# `java.util.ArrayList` only ever gets add()ed to and iterated.
java_mod = types.ModuleType("java")
@@ -346,11 +398,32 @@ def main(argv=None) -> int:
plugin.allows(other) and not plugin.allows(no_page),
)
rich_msg = FakeAdapter(FakeMessageObject(5, -1000000000200, FakeRichMessage()))
rich_bare = FakeAdapter(FakeRichMessage())
plain_msg = FakeAdapter(FakeMessageObject(5, -1000000000200, None))
check(
"settings expose the rich_messages switch",
any(getattr(row, "key", None) == "rich_messages" for row in rows),
)
check(
"rich message media is accepted by default (with or without a message)",
plugin.allows(rich_msg) and plugin.allows(rich_bare),
)
check(
"message parent without a rich message is rejected",
not plugin.allows(plain_msg),
)
plugin.set_setting("rich_messages", False)
check(
"rich_messages setting off rejects rich message media",
not plugin.allows(rich_msg) and not plugin.allows(rich_bare),
)
ivmedia = ns.get("ivmedia")
sender = ns.get("sender")
caption = getattr(sender, "CAPTION_TEXT", None)
link_entities = getattr(sender, "_link_entities", None)
entities = link_entities("https://telegra.ph/x") if link_entities else []
entities = link_entities(caption, "https://telegra.ph/x") if link_entities and caption else []
entity = entities[0] if entities else None
check(
"caption is a single text_url entity covering the whole caption",
@@ -363,6 +436,33 @@ def main(argv=None) -> int:
f"got caption={caption!r} entities={entities!r}",
)
message_link = getattr(ivmedia, "message_link", None)
if message_link is not None:
# Channel dialog ids are -(chat id) in this fake, like getDialogId().
links = (
message_link(0, FakeMessageObject(7, -100)),
message_link(0, FakeMessageObject(8, -200)),
message_link(0, FakeMessageObject(9, -300)),
message_link(0, FakeMessageObject(9, 12345)),
message_link(0, FakeMessageObject(-1, -100)),
message_link(0, FakeRichMessage()),
)
else:
links = None
check(
"message_link: public/private channels linked; groups, DMs, local and bare rich messages not",
links
== (
"https://t.me/publicchan/7",
"https://t.me/c/200/8",
None,
None,
None,
None,
),
f"got {links!r}",
)
media_at = getattr(ivmedia, "media_at", None)
no_media = getattr(ivmedia, "NoMedia", None)
raised = False