generated from exteraGramPlugins/pluginBase
Send forwarded media to the forum topic picked in the share sheet
Publish plugin / publish (push) Successful in 3s
Publish plugin / publish (push) Successful in 3s
The picker only read the selected dialog ids, so media always landed in a forum's General topic. Read ShareAlert's selectedDialogTopics as well and address each target the way ShareAlert.sendInternal() does: reply to the topic's start message in forums, or set monoForumPeer in a channel's direct-messages chat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
46503f24a0
commit
1f0a9a1a7a
6 files changed
+150
-23
No files matched your search
@@ -84,6 +84,25 @@ otherwise. On Android the dialog id of a channel is `-chat.id`, with no `-100`
|
||||
prefix. DMs, basic groups and Saved Messages have no link, so media forwarded
|
||||
from rich messages there is sent without a caption.
|
||||
|
||||
## Forum topics in the share sheet
|
||||
|
||||
When a forum is picked in `ShareAlert`, the topic lands in the protected
|
||||
`selectedDialogTopics` map. That map is keyed by the `TLRPC.Dialog` values of
|
||||
`selectedDialogs`, not by dialog id. `ShareAlert.sendInternal()` then addresses
|
||||
each chat in one of two ways:
|
||||
|
||||
- **Forum topic:** wraps `topic.topicStartMessage` in a `MessageObject`, sets
|
||||
`isTopicMainMessage = true`, and passes it as both `replyToMsg` and
|
||||
`replyToTopMsg`. `SendMessagesHelper` sends its id as `top_msg_id`. A topic's
|
||||
id is its start message's id.
|
||||
- **Mono forum** (a channel's direct-messages chat,
|
||||
`MessagesController.isMonoForum`): no reply message. Instead
|
||||
`monoForumPeer = DialogObject.getPeerDialogId(topic.from_id)`.
|
||||
|
||||
A plugin that only reads the `selectedDialogs` keys sends to the chat's
|
||||
General topic. `picker._selected_targets` reproduces the logic above inside
|
||||
`didShare()`, which runs before the sheet clears its selection.
|
||||
|
||||
## File references
|
||||
|
||||
Pass the adapter's parent object (WebPage or MessageObject) as `parentObject`
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"name": "Extended Media Forwards",
|
||||
"description": "Adds a Forward action to media opened from telegra.ph pages and rich messages (single media and carousels): downloads the media and re-sends it from your account with a link back to its source.",
|
||||
"author": "@lyuksovannyy",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"icon": "exteraPlugins/1",
|
||||
"app_version": ">=12.5.1",
|
||||
"sdk_version": ">=1.4.4.3",
|
||||
|
||||
+4
-4
@@ -82,18 +82,18 @@ class ExtendedMediaForwardsPlugin(BasePlugin):
|
||||
if context is None:
|
||||
return
|
||||
|
||||
def picked(peers, comment):
|
||||
if not peers:
|
||||
def picked(targets, comment):
|
||||
if not targets:
|
||||
return
|
||||
# The sheet sits above the viewer's window; drop it so the sent
|
||||
# message is what the user lands on.
|
||||
viewer.closePhoto(False, False)
|
||||
sender.send_media(account, media, path, is_video, caption, peers, comment, parent)
|
||||
sender.send_media(account, media, path, is_video, caption, targets, comment, parent)
|
||||
|
||||
try:
|
||||
# ShareAlert owns its own window, so it shows over PhotoViewer just
|
||||
# like the stock share button does - no need to close the viewer.
|
||||
picker.pick_chats(context, picked, ivmedia.resources_provider(viewer))
|
||||
picker.pick_chats(context, account, picked, ivmedia.resources_provider(viewer))
|
||||
except Exception as exc:
|
||||
log("[extended_media_forwards] picker failed: %s" % exc)
|
||||
sender.notify_error(str(exc))
|
||||
|
||||
+39
-9
@@ -5,12 +5,21 @@ the picker pixel-identical to the stock one. It is constructed with no payload
|
||||
(`sendingMessageObjects`, `sendingText` and `sendingFile` all null), which makes
|
||||
its send path a no-op: it selects chats, fires `ShareAlertDelegate.didShare()`
|
||||
and dismisses. The selection is read back off the (protected) `selectedDialogs`
|
||||
field and the comment off `commentTextView`.
|
||||
field, the topic picked in each forum off `selectedDialogTopics`, and the
|
||||
comment off `commentTextView`.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from android_utils import log, run_on_ui_thread
|
||||
from hook_utils import get_private_field
|
||||
|
||||
# Where to send: `peer` is the dialog id. In a forum `top_message` is the
|
||||
# topic's start message, which the stock sheet replies to so the message lands
|
||||
# in that topic. In a channel's direct-messages chat (a "mono forum") each
|
||||
# topic is one user instead, addressed through `mono_forum_peer`.
|
||||
Target = namedtuple("Target", "peer top_message mono_forum_peer")
|
||||
|
||||
_delegate_class = None
|
||||
|
||||
# ShareAlert's own constant for "keep the keyboard out of the way".
|
||||
@@ -28,25 +37,46 @@ def _make_delegate_class():
|
||||
from org.telegram.ui.Components import ShareAlert
|
||||
|
||||
class Delegate(dynamic_proxy(ShareAlert.ShareAlertDelegate)):
|
||||
def __init__(self, alert, on_picked):
|
||||
def __init__(self, alert, account, on_picked):
|
||||
super().__init__()
|
||||
self.alert = alert
|
||||
self.account = account
|
||||
self.on_picked = on_picked
|
||||
|
||||
def didCopy(self):
|
||||
return False
|
||||
|
||||
def didShare(self):
|
||||
self.on_picked(_selected_peers(self.alert), _comment(self.alert))
|
||||
self.on_picked(_selected_targets(self.alert, self.account), _comment(self.alert))
|
||||
|
||||
return Delegate
|
||||
|
||||
|
||||
def _selected_peers(alert):
|
||||
def _selected_targets(alert, account):
|
||||
"""One Target per chosen chat, resolved the way ShareAlert.sendInternal()
|
||||
resolves them. Must run on the UI thread, before the sheet clears its
|
||||
selection."""
|
||||
from org.telegram.messenger import DialogObject, MessageObject, MessagesController
|
||||
|
||||
dialogs = get_private_field(alert, "selectedDialogs")
|
||||
if dialogs is None:
|
||||
return []
|
||||
return [dialogs.keyAt(i) for i in range(dialogs.size())]
|
||||
# Keyed by the TLRPC.Dialog values of selectedDialogs, not by dialog id.
|
||||
topics = get_private_field(alert, "selectedDialogTopics")
|
||||
targets = []
|
||||
for i in range(dialogs.size()):
|
||||
peer = dialogs.keyAt(i)
|
||||
topic = topics.get(dialogs.valueAt(i)) if topics is not None else None
|
||||
top_message = None
|
||||
mono_forum_peer = 0
|
||||
if topic is not None:
|
||||
if MessagesController.getInstance(account).isMonoForum(peer):
|
||||
mono_forum_peer = DialogObject.getPeerDialogId(topic.from_id)
|
||||
elif topic.topicStartMessage is not None:
|
||||
top_message = MessageObject(account, topic.topicStartMessage, False, False)
|
||||
top_message.isTopicMainMessage = True
|
||||
targets.append(Target(peer, top_message, mono_forum_peer))
|
||||
return targets
|
||||
|
||||
|
||||
def _comment(alert):
|
||||
@@ -57,9 +87,9 @@ def _comment(alert):
|
||||
return str(text).strip() if text is not None else ""
|
||||
|
||||
|
||||
def pick_chats(context, on_picked, resources_provider=None):
|
||||
"""Shows the stock share sheet. `on_picked(dialog_ids, comment)` runs on the
|
||||
UI thread once the user confirms."""
|
||||
def pick_chats(context, account, on_picked, resources_provider=None):
|
||||
"""Shows the stock share sheet. `on_picked(targets, comment)` runs on the
|
||||
UI thread once the user confirms, with one `Target` per chosen chat."""
|
||||
global _delegate_class
|
||||
from org.telegram.ui.Components import ShareAlert
|
||||
|
||||
@@ -68,7 +98,7 @@ def pick_chats(context, on_picked, resources_provider=None):
|
||||
if _delegate_class is None:
|
||||
_delegate_class = _make_delegate_class()
|
||||
# Keep a reference on the module so the proxy is not collected early.
|
||||
delegate = _delegate_class(alert, on_picked)
|
||||
delegate = _delegate_class(alert, account, on_picked)
|
||||
alert.setDelegate(delegate)
|
||||
pick_chats._alive = delegate
|
||||
|
||||
|
||||
+19
-9
@@ -38,10 +38,10 @@ CAPTION_TEXT = "telegra.ph source"
|
||||
RICH_CAPTION_TEXT = "message source"
|
||||
|
||||
|
||||
def send_media(account, media, cached_path, is_video, caption, peers, comment, parent=None):
|
||||
def send_media(account, media, cached_path, is_video, caption, targets, comment, parent=None):
|
||||
"""Starts the download+send in the background. Safe to call on the UI thread.
|
||||
|
||||
`caption` is `(text, url)` for a linked caption or None for none. `parent`
|
||||
`targets` are `picker.Target`s. `caption` is `(text, url)` for a linked caption or None for none. `parent`
|
||||
is the object the media belongs to (WebPage or MessageObject); FileLoader
|
||||
uses it to refresh an expired file reference.
|
||||
"""
|
||||
@@ -49,10 +49,10 @@ def send_media(account, media, cached_path, is_video, caption, peers, comment, p
|
||||
def worker():
|
||||
try:
|
||||
path = cached_path or _download(account, media, is_video, parent)
|
||||
for peer in peers:
|
||||
for target in targets:
|
||||
if comment:
|
||||
_send(account, _text_params(comment, peer))
|
||||
_send(account, _media_params(account, media, path, is_video, caption, peer))
|
||||
_send(account, _text_params(comment, target))
|
||||
_send(account, _media_params(account, media, path, is_video, caption, target))
|
||||
except Exception as exc:
|
||||
log("[extended_media_forwards] send failed: %s" % exc)
|
||||
notify_error(str(exc))
|
||||
@@ -62,16 +62,26 @@ def send_media(account, media, cached_path, is_video, caption, peers, comment, p
|
||||
threading.Thread(target=worker, name="extended-media-forwards", daemon=True).start()
|
||||
|
||||
|
||||
def _text_params(text, peer):
|
||||
def _address(params, target):
|
||||
"""Points `params` at the chosen forum topic, as ShareAlert does."""
|
||||
if target.top_message is not None:
|
||||
params.replyToMsg = target.top_message
|
||||
params.replyToTopMsg = target.top_message
|
||||
if target.mono_forum_peer:
|
||||
params.monoForumPeer = target.mono_forum_peer
|
||||
return params
|
||||
|
||||
|
||||
def _text_params(text, target):
|
||||
from org.telegram.messenger import SendMessagesHelper
|
||||
|
||||
return SendMessagesHelper.SendMessageParams.of(text, peer)
|
||||
return _address(SendMessagesHelper.SendMessageParams.of(text, target.peer), target)
|
||||
|
||||
|
||||
def _media_params(account, media, path, is_video, caption, peer):
|
||||
def _media_params(account, media, path, is_video, caption, target):
|
||||
from org.telegram.messenger import SendMessagesHelper
|
||||
|
||||
params = SendMessagesHelper.SendMessageParams.of(None, peer)
|
||||
params = _address(SendMessagesHelper.SendMessageParams.of(None, target.peer), target)
|
||||
if caption is not None:
|
||||
text, url = caption
|
||||
params.caption = text
|
||||
|
||||
@@ -463,6 +463,74 @@ def main(argv=None) -> int:
|
||||
f"got {links!r}",
|
||||
)
|
||||
|
||||
picker = ns.get("picker")
|
||||
selected_targets = getattr(picker, "_selected_targets", None)
|
||||
address = getattr(sender, "_address", None)
|
||||
targets = None
|
||||
addressed = None
|
||||
if selected_targets is not None and address is not None:
|
||||
|
||||
class SparseArray:
|
||||
def __init__(self, pairs):
|
||||
self.pairs = pairs
|
||||
|
||||
def size(self):
|
||||
return len(self.pairs)
|
||||
|
||||
def keyAt(self, i):
|
||||
return self.pairs[i][0]
|
||||
|
||||
def valueAt(self, i):
|
||||
return self.pairs[i][1]
|
||||
|
||||
class TopicMessageObject:
|
||||
def __init__(self, account, message, generate_layout, check_media):
|
||||
self.message = message
|
||||
self.isTopicMainMessage = False
|
||||
|
||||
plain, forum, mono = object(), object(), object()
|
||||
topic = types.SimpleNamespace(topicStartMessage="start-msg", from_id="user-peer")
|
||||
mono_topic = types.SimpleNamespace(topicStartMessage="unused", from_id="dm-user")
|
||||
alert = types.SimpleNamespace(
|
||||
selectedDialogs=SparseArray([(11, plain), (-22, forum), (-33, mono)]),
|
||||
selectedDialogTopics={forum: topic, mono: mono_topic},
|
||||
)
|
||||
messenger = sys.modules["org.telegram.messenger"]
|
||||
saved = messenger.MessageObject
|
||||
messenger.MessageObject = TopicMessageObject
|
||||
messenger.DialogObject = types.SimpleNamespace(getPeerDialogId=lambda peer: 4242)
|
||||
messenger.MessagesController = types.SimpleNamespace(
|
||||
getInstance=lambda account: types.SimpleNamespace(isMonoForum=lambda d: d == -33)
|
||||
)
|
||||
try:
|
||||
targets = selected_targets(alert, 0)
|
||||
finally:
|
||||
messenger.MessageObject = saved
|
||||
params = [
|
||||
address(types.SimpleNamespace(replyToMsg=None, replyToTopMsg=None, monoForumPeer=0), t)
|
||||
for t in targets
|
||||
]
|
||||
addressed = [
|
||||
(
|
||||
t.peer,
|
||||
getattr(p.replyToTopMsg, "message", None),
|
||||
p.replyToMsg is p.replyToTopMsg,
|
||||
getattr(p.replyToTopMsg, "isTopicMainMessage", None),
|
||||
p.monoForumPeer,
|
||||
)
|
||||
for t, p in zip(targets, params)
|
||||
]
|
||||
check(
|
||||
"selected forum topic is replied to; mono-forum topic sets monoForumPeer; plain chat untouched",
|
||||
addressed
|
||||
== [
|
||||
(11, None, True, None, 0),
|
||||
(-22, "start-msg", True, True, 0),
|
||||
(-33, None, True, None, 4242),
|
||||
],
|
||||
f"got {addressed!r}",
|
||||
)
|
||||
|
||||
media_at = getattr(ivmedia, "media_at", None)
|
||||
no_media = getattr(ivmedia, "NoMedia", None)
|
||||
raised = False
|
||||
|
||||
Reference in new issue
Block a user