Files
messageLogger/src/hooks/deletes.py
T
lyuksovannyyandClaude Opus 5.5 0176f7099d
Publish plugin / publish (push) Successful in 4s
Message Logger: keep own deletions, remove from view, reset marks
- Delete dialog: "Keep message for myself" checkbox keeps your deletion on
  this device (marked deleted); unticked, the message is hidden and never
  restored. Already deleted messages get a plain "Remove from view" dialog
  (fixes the chat staying dimmed/touch-blocked after it).
- "Reset deleted/edited marks" in the chat menu (Plugins) and, for all
  chats, in the plugin settings.
- Fix restored text-only messages showing as empty bubbles (rich text
  wrappers like links lost their text); missing settings icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:59:48 +02:00

153 lines
5.6 KiB
Python

"""Keeps messages others delete: the delete updates are dropped (the message
stays in the app's cache and chat) and the ids are remembered as deleted.
Deletions the user makes here go through MessagesController.deleteMessages
(both overloads start `(messages, randoms, encryptedChat, dialogId, ...)`);
those ids are let through when their echo update arrives.
TL_updateDeleteMessages (private chats, basic groups) carries no chat: ids
are per account, so the dialog is looked up in the app's own database on the
storage queue.
"""
from base_plugin import HookResult, HookStrategy, MethodHook
import diag
import peers
import store
from api import ApiError
from config import KEEP_DELETED, REPORT_DELETIONS
from jutil import field, hook_methods, in_background, java_ints
UPDATES = ("TL_updateDeleteMessages", "TL_updateDeleteChannelMessages")
class Deletes:
def __init__(self, plugin, api, chats, dialog=None):
self.plugin = plugin
self.api = api
self.chats = chats
self.dialog = dialog # hooks/delete_dialog.DeleteDialog
self.handles = []
def install(self):
for name in UPDATES:
self.plugin.add_hook(name)
deletes = self
class BeforeDelete(MethodHook):
def before_hooked_method(self, param):
try:
deletes.on_own_delete(param)
except Exception as exc:
diag.error("deletes.own", exc)
self.handles += hook_methods(
self.plugin,
"org.telegram.messenger.MessagesController",
"deleteMessages",
BeforeDelete(),
)
def uninstall(self):
for name in UPDATES:
try:
self.plugin.remove_hook(name)
except Exception:
pass
for handle in self.handles:
try:
self.plugin.unhook_method(handle)
except Exception:
pass
self.handles = []
def on_own_delete(self, param):
args = list(param.args or [])
if len(args) < 4:
return
ids = [i for i in java_ints(args[0]) if i > 0]
if not ids:
return
account = int(field(param.thisObject, "currentAccount") or 0)
dialog_id = int(args[3] or 0)
kept = self.dialog.apply_choice(account, dialog_id, ids) if self.dialog else []
if kept:
self.chats.refresh_dialog(account, dialog_id, kept)
ids = [i for i in ids if i not in kept]
if not ids:
return
store.note_own_deletion(account, ids)
store.forget_deleted(account, dialog_id, ids)
def on_update(self, name, account, update):
ids = java_ints(update.messages)
others = [i for i in ids if not store.is_own_deletion(account, i)]
if not others:
return HookResult()
if name == "TL_updateDeleteChannelMessages":
self._deleted(account, -int(update.channel_id), others)
else:
self._resolve_dialogs(account, others)
if not self.plugin.get_setting(KEEP_DELETED, True):
return HookResult()
diag.log(f"kept {name} {others}")
if len(others) == len(ids):
return HookResult(strategy=HookStrategy.CANCEL)
from java import jint
from java.util import ArrayList
own = ArrayList()
for i in ids:
if i not in others:
own.add(jint(i)) # a plain int would be boxed as Long
update.messages = own
return HookResult(strategy=HookStrategy.MODIFY, update=update)
def _deleted(self, account, dialog_id, ids):
store.mark_deleted(account, dialog_id, ids)
self.chats.refresh_dialog(account, dialog_id, ids)
# Check the chat again once the server had the report.
self.chats.changed(account, dialog_id, 3000)
if self.plugin.get_setting(REPORT_DELETIONS, True) and self.api.configured:
in_background(lambda: self._report(account, dialog_id, ids))
def _report(self, account, dialog_id, ids):
chat_id = peers.marked_chat_id(account, dialog_id)
if chat_id is None:
return
try:
self.api.deletions(peers.own_user_id(account), chat_id, ids)
except ApiError as exc:
diag.log(f"report {chat_id}: {exc}")
def _resolve_dialogs(self, account, ids):
"""Dialogs of per-account message ids, from the app's database."""
from java import dynamic_proxy
from java.lang import Runnable
from org.telegram.messenger import MessagesStorage
storage = MessagesStorage.getInstance(account)
deletes = self
class Lookup(dynamic_proxy(Runnable)):
def run(self):
try:
found = {}
query = "SELECT mid, uid FROM messages_v2 WHERE mid IN (%s)" % (
",".join(str(int(i)) for i in ids)
)
cursor = storage.getDatabase().queryFinalized(query)
while cursor.next():
uid = int(cursor.longValue(1))
if uid < 0 and peers.is_channel_dialog(account, uid):
continue # channel ids are numbered per channel
found.setdefault(uid, []).append(int(cursor.intValue(0)))
cursor.dispose()
for dialog_id, mids in found.items():
deletes._deleted(account, dialog_id, mids)
except Exception as exc:
diag.error("deletes.resolve", exc)
storage.getStorageQueue().postRunnable(Lookup())