"""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())