generated from exteraGramPlugins/pluginBase
Publish plugin / publish (push) Successful in 6s
- Hidden deleted messages are marked on the server (local marks migrated); "Reveal hidden messages" in the chat menu, Unhide in the message menu and the selection bar. - Chats an account lost are reported, listed among its chats again and opened read-only with their logged messages, all deleted. - Logged chat photos: a removed chat's avatar, and in a chat's photos the ones it no longer has, translucent with a trash icon in the viewer's previews. - Fix: the deleted-post comments thread is answered locally again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
7.2 KiB
Python
203 lines
7.2 KiB
Python
"""Deleted messages removed from view ("hidden"), marked on the server so
|
|
every client skips them; a chat can reveal its hidden messages again.
|
|
|
|
- `flush` sends the marks waiting in the store to the server;
|
|
- the chat menu toggles revealing (per chat, until the app restarts): the
|
|
hidden deleted messages come back from the logger, then go again;
|
|
- a revealed hidden message has "Unhide" in its menu (hooks/menu.py), and
|
|
while revealing, the selection bar gets an Unhide button for those
|
|
selected (ChatActivity.createActionMode adds the bar's items,
|
|
updateActionModeTitle runs on every selection change; probed on 12.10.6).
|
|
"""
|
|
|
|
import threading
|
|
|
|
from base_plugin import MethodHook
|
|
|
|
import diag
|
|
import peers
|
|
import store
|
|
from api import ApiError
|
|
from jutil import field, hook_methods, in_background, on_ui
|
|
|
|
CHAT = "org.telegram.ui.ChatActivity"
|
|
_UNHIDE_ID = 0x4D4C01 # the selection bar item
|
|
|
|
|
|
def _bulletin(text, error=False):
|
|
def show():
|
|
from ui.bulletin import BulletinHelper
|
|
|
|
(BulletinHelper.show_error if error else BulletinHelper.show_info)(text)
|
|
|
|
on_ui(show)
|
|
|
|
|
|
def _selected(chat):
|
|
"""MessageObjects selected in the chat (both halves of a merged chat)."""
|
|
out = []
|
|
for array in field(chat, "selectedMessagesIds") or []:
|
|
if array is None:
|
|
continue
|
|
for index in range(array.size()):
|
|
message = array.valueAt(index)
|
|
if message is not None:
|
|
out.append(message)
|
|
return out
|
|
|
|
|
|
def is_revealed_hidden(message):
|
|
"""A hidden deleted message shown because its chat reveals them."""
|
|
if message is None or int(message.getId()) <= 0:
|
|
return False
|
|
account, dialog_id = int(message.currentAccount), int(message.getDialogId())
|
|
message_id = int(message.getId())
|
|
return (
|
|
store.is_revealed(account, dialog_id)
|
|
and store.is_deleted(account, dialog_id, message_id)
|
|
and store.is_hidden(account, dialog_id, message_id)
|
|
)
|
|
|
|
|
|
class Hidden:
|
|
def __init__(self, plugin, api, chats):
|
|
self.plugin = plugin
|
|
self.api = api
|
|
self.chats = chats # hooks/chat.ChatSync
|
|
self.handles = []
|
|
self._flushing = threading.Lock()
|
|
self._items = {} # ChatActivity classGuid -> the bar's Unhide item
|
|
|
|
def install(self):
|
|
hidden = self
|
|
|
|
class AfterActionMode(MethodHook):
|
|
def after_hooked_method(self, param):
|
|
try:
|
|
hidden.add_bar_item(param.thisObject)
|
|
except Exception as exc:
|
|
diag.error("hidden.bar", exc)
|
|
|
|
class AfterTitle(MethodHook):
|
|
def after_hooked_method(self, param):
|
|
try:
|
|
hidden.update_bar_item(param.thisObject)
|
|
except Exception as exc:
|
|
diag.error("hidden.title", exc)
|
|
|
|
self.handles += hook_methods(self.plugin, CHAT, "createActionMode", AfterActionMode())
|
|
self.handles += hook_methods(self.plugin, CHAT, "updateActionModeTitle", AfterTitle())
|
|
in_background(self.flush)
|
|
|
|
def uninstall(self):
|
|
for handle in self.handles:
|
|
try:
|
|
self.plugin.unhook_method(handle)
|
|
except Exception:
|
|
pass
|
|
self.handles = []
|
|
|
|
# the server
|
|
|
|
def flush(self):
|
|
"""Sends hidden marks waiting in the store (background)."""
|
|
if not self.api.configured or not self._flushing.acquire(blocking=False):
|
|
return
|
|
try:
|
|
for (account, dialog_id, hidden), ids in store.pending_hidden().items():
|
|
me = peers.own_user_id(account)
|
|
chat_id = peers.marked_chat_id(account, dialog_id)
|
|
if not me or chat_id is None:
|
|
continue
|
|
try:
|
|
self.api.hidden(me, chat_id, ids, hidden)
|
|
except ApiError as exc:
|
|
diag.log(f"hidden {chat_id}: {exc}")
|
|
continue
|
|
store.hidden_sent(account, dialog_id, ids, hidden)
|
|
diag.log(f"{'hid' if hidden else 'unhid'} {chat_id}: {ids}")
|
|
finally:
|
|
self._flushing.release()
|
|
|
|
def hide(self, account, dialog_id, ids, hidden=True):
|
|
store.set_hidden(account, dialog_id, ids, hidden)
|
|
in_background(self.flush)
|
|
|
|
# revealing (chat menu)
|
|
|
|
def toggle_reveal(self, fragment):
|
|
account = int(fragment.getCurrentAccount())
|
|
dialog_id = int(fragment.getDialogId())
|
|
on = not store.is_revealed(account, dialog_id)
|
|
store.reveal(account, dialog_id, on)
|
|
if on:
|
|
count = self.chats.restore_hidden(fragment)
|
|
# And the server's answer, for those not cached.
|
|
store.expire_coverage(account, dialog_id)
|
|
self.chats.schedule(fragment)
|
|
_bulletin(
|
|
f"Message Logger: revealing hidden deleted messages ({count} loaded)"
|
|
if count
|
|
else "Message Logger: revealing hidden deleted messages"
|
|
)
|
|
else:
|
|
ids = store.hidden_deleted(account, dialog_id)
|
|
self.chats.drop_from_view(account, dialog_id, ids)
|
|
_bulletin("Message Logger: hidden deleted messages are hidden again")
|
|
|
|
# unhiding
|
|
|
|
def unhide(self, messages):
|
|
by_chat = {}
|
|
for message in messages:
|
|
if is_revealed_hidden(message):
|
|
key = (int(message.currentAccount), int(message.getDialogId()))
|
|
by_chat.setdefault(key, []).append(int(message.getId()))
|
|
for (account, dialog_id), ids in by_chat.items():
|
|
self.hide(account, dialog_id, ids, hidden=False)
|
|
self.chats.refresh_dialog(account, dialog_id, ids)
|
|
count = sum(len(ids) for ids in by_chat.values())
|
|
if count:
|
|
_bulletin(f"Message Logger: {count} message{'s' if count > 1 else ''} unhidden")
|
|
|
|
# the selection bar
|
|
|
|
def add_bar_item(self, chat):
|
|
"""The bar's Unhide item (also for a chat opened before the plugin
|
|
loaded: its bar exists already)."""
|
|
bar = chat.getActionBar()
|
|
menu = bar.createActionMode() if bar is not None else None
|
|
if menu is None:
|
|
return None
|
|
from android.view import View
|
|
from android_utils import OnClickListener
|
|
from org.telegram.messenger import AndroidUtilities, R
|
|
|
|
item = menu.getItem(_UNHIDE_ID)
|
|
if item is None:
|
|
item = menu.addItemWithWidth(
|
|
_UNHIDE_ID, R.drawable.msg_views, AndroidUtilities.dp(54)
|
|
)
|
|
item.setContentDescription("Unhide")
|
|
item.setVisibility(View.GONE)
|
|
|
|
def chosen(_view):
|
|
self.unhide(_selected(chat))
|
|
chat.clearSelectionMode()
|
|
|
|
item.setOnClickListener(OnClickListener(chosen))
|
|
self._items[int(chat.getClassGuid())] = item
|
|
return item
|
|
|
|
def update_bar_item(self, chat):
|
|
item = self._items.get(int(chat.getClassGuid())) or self.add_bar_item(chat)
|
|
if item is None:
|
|
return
|
|
from android.view import View
|
|
|
|
account, dialog_id = int(chat.getCurrentAccount()), int(chat.getDialogId())
|
|
show = store.is_revealed(account, dialog_id) and any(
|
|
is_revealed_hidden(m) for m in _selected(chat)
|
|
)
|
|
item.setVisibility(View.VISIBLE if show else View.GONE)
|