generated from exteraGramPlugins/pluginBase
Message Logger: hidden messages on the server, removed chats, old chat photos
Publish plugin / publish (push) Successful in 6s
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:
1 parent
0176f7099d
commit
362b358d8d
15 files changed
+1821
-125
No files matched your search
@@ -0,0 +1,266 @@
|
||||
"""The history of a removed chat, rebuilt from the logger: the logged messages
|
||||
(`/chats/{id}/history`), each from its copy in a log group when an account
|
||||
here can read it (header stripped, media included), else from the server's
|
||||
text with its media marked unavailable. All of them are deleted for the
|
||||
account; they are cached as such (store), so they draw like any deleted
|
||||
message.
|
||||
|
||||
`History` holds one chat's messages, newest first, and answers the app's
|
||||
getHistory/getMessages for it (hooks/removed_chats.py).
|
||||
"""
|
||||
|
||||
import threading
|
||||
|
||||
import accounts
|
||||
import diag
|
||||
import peers
|
||||
import store
|
||||
import tg
|
||||
from api import ApiError
|
||||
from views.history import _MEDIA_NAMES, _entities, _epoch, _utf16
|
||||
|
||||
_PAGES = 3 # history pages (of 100) loaded when the chat opens
|
||||
_COPIES_PER_REQUEST = 100
|
||||
|
||||
|
||||
def stand_in_user(controller, peer_id, name):
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
user = controller.getUser(int(peer_id))
|
||||
if user is None:
|
||||
user = TLRPC.TL_user()
|
||||
user.id = int(peer_id)
|
||||
user.first_name = name or "Deleted"
|
||||
user.flags |= 2
|
||||
return user
|
||||
|
||||
|
||||
def stand_in_chat(controller, chat_id, title, kind):
|
||||
"""The app's chat (cached or stored), else a stand-in of the right kind."""
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
chat = controller.getChat(int(chat_id))
|
||||
if chat is not None:
|
||||
return chat
|
||||
if kind == "group":
|
||||
chat = TLRPC.TL_chat()
|
||||
else:
|
||||
chat = TLRPC.TL_channel()
|
||||
chat.broadcast = kind == "channel"
|
||||
chat.megagroup = kind != "channel"
|
||||
chat.id = int(chat_id)
|
||||
chat.title = title or "Chat"
|
||||
return chat
|
||||
|
||||
|
||||
class History:
|
||||
def __init__(self, api, account, dialog_id, chat_id):
|
||||
self.api = api
|
||||
self.account = account
|
||||
self.dialog_id = dialog_id
|
||||
self.chat_id = chat_id
|
||||
self.messages = [] # TL messages, newest first
|
||||
self.users = {}
|
||||
self.chats = {}
|
||||
self.loaded = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# loading (background)
|
||||
|
||||
def load(self):
|
||||
"""Fetches and builds the chat's history once; later calls wait."""
|
||||
with self._lock:
|
||||
if self.loaded:
|
||||
return
|
||||
try:
|
||||
self._load()
|
||||
except Exception as exc:
|
||||
diag.error("removed_chat.load", exc)
|
||||
self.loaded = True
|
||||
|
||||
def _load(self):
|
||||
me = peers.own_user_id(self.account)
|
||||
items, before, master = [], 0, None
|
||||
for _page in range(_PAGES):
|
||||
try:
|
||||
result = self.api.history(me, self.chat_id, before)
|
||||
except ApiError as exc:
|
||||
diag.log(f"removed chat {self.chat_id}: {exc}")
|
||||
break
|
||||
page = (result or {}).get("messages") or []
|
||||
if not page:
|
||||
break
|
||||
master = result.get("master_tg_id")
|
||||
items += page
|
||||
before = min(int(m["message_id"]) for m in page)
|
||||
if len(page) < 100:
|
||||
break
|
||||
revealed = store.is_revealed(self.account, self.dialog_id)
|
||||
items = [i for i in items if revealed or not i.get("hidden_at")]
|
||||
store.put_server_states(self.account, self.dialog_id, items)
|
||||
copies = self._copies(items, master)
|
||||
built = []
|
||||
for item in items:
|
||||
try:
|
||||
built.append(self._message(item, copies.get(self._pointer(item))))
|
||||
except Exception as exc:
|
||||
diag.error("removed_chat.message", exc)
|
||||
self.messages = sorted(built, key=lambda m: -int(m.id))
|
||||
diag.log(f"removed chat {self.chat_id}: {len(self.messages)} messages")
|
||||
|
||||
@staticmethod
|
||||
def _pointer(item):
|
||||
logged = item.get("logged") or {}
|
||||
if not logged.get("chat_id") or not logged.get("message_id"):
|
||||
return None
|
||||
return int(logged["chat_id"]), int(logged["message_id"])
|
||||
|
||||
def _copies(self, items, master):
|
||||
"""Logged copies by (log chat, message), read through an account here
|
||||
that can, per log group."""
|
||||
by_group = {}
|
||||
for item in items:
|
||||
pointer = self._pointer(item)
|
||||
if pointer is not None:
|
||||
by_group.setdefault(pointer[0], set()).add(pointer[1])
|
||||
found = {}
|
||||
for log_chat, ids in by_group.items():
|
||||
channel_id = peers.channel_id_from_marked(log_chat)
|
||||
readers = accounts.with_access(
|
||||
-channel_id, prefer=(self.account, accounts.slot_of(master))
|
||||
)
|
||||
if not readers:
|
||||
continue
|
||||
ordered = sorted(ids)
|
||||
for start in range(0, len(ordered), _COPIES_PER_REQUEST):
|
||||
chunk = ordered[start : start + _COPIES_PER_REQUEST]
|
||||
for message_id, copy in _fetch(readers[0], -channel_id, chunk).items():
|
||||
found[(log_chat, message_id)] = copy
|
||||
return found
|
||||
|
||||
# building
|
||||
|
||||
def _message(self, item, copy):
|
||||
from org.telegram.messenger import MessagesController
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
from hooks.chat import _android_peer_id, build_from_copy
|
||||
|
||||
controller = MessagesController.getInstance(self.account)
|
||||
message_id = int(item["message_id"])
|
||||
if copy is not None and not isinstance(copy, TLRPC.TL_messageEmpty):
|
||||
msg = build_from_copy(
|
||||
self.account, self.dialog_id, message_id, item, copy, True
|
||||
).messageOwner
|
||||
else:
|
||||
msg = from_text(self.account, self.dialog_id, item)
|
||||
sender = item.get("sender_id")
|
||||
if sender:
|
||||
peer_id = _android_peer_id(int(sender))
|
||||
if peer_id > 0:
|
||||
user = stand_in_user(controller, peer_id, item.get("sender_name"))
|
||||
self.users[int(user.id)] = user
|
||||
else:
|
||||
chat = stand_in_chat(controller, -peer_id, item.get("sender_name"), "channel")
|
||||
self.chats[int(chat.id)] = chat
|
||||
if item.get("reply_to"):
|
||||
reply = TLRPC.TL_messageReplyHeader()
|
||||
reply.reply_to_msg_id = int(item["reply_to"])
|
||||
reply.flags |= 16
|
||||
msg.reply_to = reply
|
||||
msg.flags |= 8
|
||||
return msg
|
||||
|
||||
# answering the app's requests
|
||||
|
||||
def response(self, picked):
|
||||
from java.util import ArrayList
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
response = TLRPC.TL_messages_messages()
|
||||
response.messages = ArrayList()
|
||||
for message in picked:
|
||||
response.messages.add(message)
|
||||
response.users = ArrayList()
|
||||
for user in self.users.values():
|
||||
response.users.add(user)
|
||||
response.chats = ArrayList()
|
||||
for chat in self.chats.values():
|
||||
response.chats.add(chat)
|
||||
return response
|
||||
|
||||
def page(self, offset_id, add_offset, limit):
|
||||
"""getHistory's slice: newest first, from `offset_id` (exclusive; 0:
|
||||
the newest) moved by `add_offset` (negative: newer ones too)."""
|
||||
messages = self.messages
|
||||
if offset_id:
|
||||
start = next(
|
||||
(i for i, m in enumerate(messages) if int(m.id) < offset_id),
|
||||
len(messages),
|
||||
)
|
||||
else:
|
||||
start = 0
|
||||
start = max(0, start + add_offset)
|
||||
return messages[start : start + max(1, limit)]
|
||||
|
||||
def by_ids(self, ids):
|
||||
wanted = set(int(i) for i in ids)
|
||||
return [m for m in self.messages if int(m.id) in wanted]
|
||||
|
||||
|
||||
def from_text(account, dialog_id, item):
|
||||
"""A logged message from the server's text; each media is marked
|
||||
unavailable after it."""
|
||||
from org.telegram.messenger import MessagesController
|
||||
from org.telegram.tgnet import TLRPC
|
||||
|
||||
from hooks.chat import _android_peer_id
|
||||
|
||||
controller = MessagesController.getInstance(account)
|
||||
msg = TLRPC.TL_message()
|
||||
msg.id = int(item["message_id"])
|
||||
msg.dialog_id = dialog_id
|
||||
msg.peer_id = controller.getPeer(dialog_id)
|
||||
sender = item.get("sender_id")
|
||||
msg.from_id = (
|
||||
controller.getPeer(_android_peer_id(int(sender))) if sender else msg.peer_id
|
||||
)
|
||||
msg.flags |= 256
|
||||
msg.date = _epoch(item.get("source_date"))
|
||||
if int(item.get("edit_count") or 0) > 0:
|
||||
msg.edit_date = _epoch(item.get("edit_date")) or msg.date
|
||||
msg.flags |= 32768
|
||||
text = item.get("text") or ""
|
||||
entities = _entities(item.get("entities") or [])
|
||||
notes = [
|
||||
f"🚫 {_MEDIA_NAMES.get(kind, 'Media')} unavailable"
|
||||
for kind in item.get("media") or []
|
||||
]
|
||||
if notes:
|
||||
note = "\n".join(notes)
|
||||
start = _utf16(text) + (2 if text else 0)
|
||||
text = (text + "\n\n" if text else "") + note
|
||||
italic = TLRPC.TL_messageEntityItalic()
|
||||
italic.offset = start
|
||||
italic.length = _utf16(note)
|
||||
entities.add(italic)
|
||||
msg.message = text or "(empty)"
|
||||
msg.entities = entities
|
||||
msg.out = sender is not None and int(sender) == peers.own_user_id(account)
|
||||
return msg
|
||||
|
||||
|
||||
def _fetch(reader, dialog_id, ids):
|
||||
"""Messages `ids` of a log group through `reader` (blocking)."""
|
||||
done = threading.Event()
|
||||
box = {}
|
||||
|
||||
def callback(found, error):
|
||||
if found is None:
|
||||
diag.log(f"removed chat copies unreadable: {error}")
|
||||
box["found"] = found or {}
|
||||
done.set()
|
||||
|
||||
tg.get_messages(reader, dialog_id, ids, callback)
|
||||
done.wait(20)
|
||||
return box.get("found") or {}
|
||||
Reference in new issue
Block a user