generated from exteraGramPlugins/pluginBase
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f0a9a1a7a |
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
|
prefix. DMs, basic groups and Saved Messages have no link, so media forwarded
|
||||||
from rich messages there is sent without a caption.
|
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
|
## File references
|
||||||
|
|
||||||
Pass the adapter's parent object (WebPage or MessageObject) as `parentObject`
|
Pass the adapter's parent object (WebPage or MessageObject) as `parentObject`
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"name": "Extended Media Forwards",
|
"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.",
|
"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",
|
"author": "@lyuksovannyy",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"icon": "exteraPlugins/1",
|
"icon": "exteraPlugins/1",
|
||||||
"app_version": ">=12.5.1",
|
"app_version": ">=12.5.1",
|
||||||
"sdk_version": ">=1.4.4.3",
|
"sdk_version": ">=1.4.4.3",
|
||||||
|
|||||||
+4
-4
@@ -82,18 +82,18 @@ class ExtendedMediaForwardsPlugin(BasePlugin):
|
|||||||
if context is None:
|
if context is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
def picked(peers, comment):
|
def picked(targets, comment):
|
||||||
if not peers:
|
if not targets:
|
||||||
return
|
return
|
||||||
# The sheet sits above the viewer's window; drop it so the sent
|
# The sheet sits above the viewer's window; drop it so the sent
|
||||||
# message is what the user lands on.
|
# message is what the user lands on.
|
||||||
viewer.closePhoto(False, False)
|
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:
|
try:
|
||||||
# ShareAlert owns its own window, so it shows over PhotoViewer just
|
# ShareAlert owns its own window, so it shows over PhotoViewer just
|
||||||
# like the stock share button does - no need to close the viewer.
|
# 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:
|
except Exception as exc:
|
||||||
log("[extended_media_forwards] picker failed: %s" % exc)
|
log("[extended_media_forwards] picker failed: %s" % exc)
|
||||||
sender.notify_error(str(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
|
(`sendingMessageObjects`, `sendingText` and `sendingFile` all null), which makes
|
||||||
its send path a no-op: it selects chats, fires `ShareAlertDelegate.didShare()`
|
its send path a no-op: it selects chats, fires `ShareAlertDelegate.didShare()`
|
||||||
and dismisses. The selection is read back off the (protected) `selectedDialogs`
|
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 android_utils import log, run_on_ui_thread
|
||||||
from hook_utils import get_private_field
|
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
|
_delegate_class = None
|
||||||
|
|
||||||
# ShareAlert's own constant for "keep the keyboard out of the way".
|
# 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
|
from org.telegram.ui.Components import ShareAlert
|
||||||
|
|
||||||
class Delegate(dynamic_proxy(ShareAlert.ShareAlertDelegate)):
|
class Delegate(dynamic_proxy(ShareAlert.ShareAlertDelegate)):
|
||||||
def __init__(self, alert, on_picked):
|
def __init__(self, alert, account, on_picked):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.alert = alert
|
self.alert = alert
|
||||||
|
self.account = account
|
||||||
self.on_picked = on_picked
|
self.on_picked = on_picked
|
||||||
|
|
||||||
def didCopy(self):
|
def didCopy(self):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def didShare(self):
|
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
|
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")
|
dialogs = get_private_field(alert, "selectedDialogs")
|
||||||
if dialogs is None:
|
if dialogs is None:
|
||||||
return []
|
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):
|
def _comment(alert):
|
||||||
@@ -57,9 +87,9 @@ def _comment(alert):
|
|||||||
return str(text).strip() if text is not None else ""
|
return str(text).strip() if text is not None else ""
|
||||||
|
|
||||||
|
|
||||||
def pick_chats(context, on_picked, resources_provider=None):
|
def pick_chats(context, account, on_picked, resources_provider=None):
|
||||||
"""Shows the stock share sheet. `on_picked(dialog_ids, comment)` runs on the
|
"""Shows the stock share sheet. `on_picked(targets, comment)` runs on the
|
||||||
UI thread once the user confirms."""
|
UI thread once the user confirms, with one `Target` per chosen chat."""
|
||||||
global _delegate_class
|
global _delegate_class
|
||||||
from org.telegram.ui.Components import ShareAlert
|
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:
|
if _delegate_class is None:
|
||||||
_delegate_class = _make_delegate_class()
|
_delegate_class = _make_delegate_class()
|
||||||
# Keep a reference on the module so the proxy is not collected early.
|
# 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)
|
alert.setDelegate(delegate)
|
||||||
pick_chats._alive = delegate
|
pick_chats._alive = delegate
|
||||||
|
|
||||||
|
|||||||
+19
-9
@@ -38,10 +38,10 @@ CAPTION_TEXT = "telegra.ph source"
|
|||||||
RICH_CAPTION_TEXT = "message 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.
|
"""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
|
is the object the media belongs to (WebPage or MessageObject); FileLoader
|
||||||
uses it to refresh an expired file reference.
|
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():
|
def worker():
|
||||||
try:
|
try:
|
||||||
path = cached_path or _download(account, media, is_video, parent)
|
path = cached_path or _download(account, media, is_video, parent)
|
||||||
for peer in peers:
|
for target in targets:
|
||||||
if comment:
|
if comment:
|
||||||
_send(account, _text_params(comment, peer))
|
_send(account, _text_params(comment, target))
|
||||||
_send(account, _media_params(account, media, path, is_video, caption, peer))
|
_send(account, _media_params(account, media, path, is_video, caption, target))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log("[extended_media_forwards] send failed: %s" % exc)
|
log("[extended_media_forwards] send failed: %s" % exc)
|
||||||
notify_error(str(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()
|
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
|
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
|
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:
|
if caption is not None:
|
||||||
text, url = caption
|
text, url = caption
|
||||||
params.caption = text
|
params.caption = text
|
||||||
|
|||||||
@@ -463,6 +463,74 @@ def main(argv=None) -> int:
|
|||||||
f"got {links!r}",
|
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)
|
media_at = getattr(ivmedia, "media_at", None)
|
||||||
no_media = getattr(ivmedia, "NoMedia", None)
|
no_media = getattr(ivmedia, "NoMedia", None)
|
||||||
raised = False
|
raised = False
|
||||||
|
|||||||
Reference in new issue
Block a user