Message Logger: hidden messages on the server, removed chats, old chat photos
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>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-10 23:12:33 +02:00
1 parent 0176f7099d
commit 362b358d8d
15 files changed
+1821 -125

No files matched your search

+202
View File
@@ -0,0 +1,202 @@
"""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)