Message Logger: show logged edits and deletions from telegram-panel [no bump]
Publish plugin / publish (push) Successful in 4s

Keeps deleted messages (75% opacity, red icon-pack trash icon), restores
logged messages deleted while away, reports deletions, and shows edit
history as a mock chat (Edit history in the message menu, Open topic).
Message links and history open as any signed-in account with access.
Per-master API tokens; chat state synced per checked range with an hour's
validity, cached in SQLite with a configurable lifetime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
lyuksovannyyandClaude Opus 5.5 committed 2026-10-08 23:55:23 +02:00
1 parent 80f1d5e55c
commit 9b2f26a845
26 files changed
+2928 -112

No files matched your search

+145
View File
@@ -0,0 +1,145 @@
"""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):
self.plugin = plugin
self.api = api
self.chats = chats
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)
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())