feat: add realtime capturing

This commit is contained in:
hh committed 2026-05-29 18:19:06 +02:00
1 parent 920a0235e2
commit 3c1a12750c
29 files changed
+967 -47

No files matched your search

+2 -2
View File
@@ -1,3 +1,3 @@
from .dialog_filters import dialog_filter_handler
from userbot.handlers import deletes, edits, messages, raw
__all__ = ["dialog_filter_handler"]
handlers = messages.handlers + edits.handlers + deletes.handlers + raw.handlers
+28
View File
@@ -0,0 +1,28 @@
from pyrogram.types import Message
from userbot import PyroClient
from userbot.modules.capture import repository
from userbot.modules.capture.repository import CHANNEL_ID_THRESHOLD
@PyroClient.on_deleted_messages()
async def on_deleted_messages(client: PyroClient, messages: list[Message]) -> None:
ctx = client.capture
if ctx is None:
return
box: list[int] = []
channels: dict[int, list[int]] = {}
for message in messages:
chat = message.chat
chat_id = chat.id if chat is not None else None
if chat_id is None or chat_id > CHANNEL_ID_THRESHOLD:
box.append(message.id)
else:
channels.setdefault(chat_id, []).append(message.id)
if box:
await repository.mark_deleted_box(ctx.pool, ctx.account_id, box)
for chat_id, ids in channels.items():
await repository.mark_deleted_channel(ctx.pool, ctx.account_id, chat_id, ids)
handlers = on_deleted_messages.handlers
@@ -1,20 +0,0 @@
from pyrogram import Client, raw
from pyrogram.handlers import RawUpdateHandler
from userbot.folders import FolderCache
_FILTER_UPDATES = (
raw.types.UpdateDialogFilter,
raw.types.UpdateDialogFilters,
raw.types.UpdateDialogFilterOrder,
)
def dialog_filter_handler(cache: FolderCache) -> RawUpdateHandler:
async def on_update(
_client: Client, update: raw.base.Update, _users: dict, _chats: dict
) -> None:
if isinstance(update, _FILTER_UPDATES):
await cache.refresh()
return RawUpdateHandler(on_update)
+36
View File
@@ -0,0 +1,36 @@
from pyrogram.types import Message
from userbot import PyroClient
from userbot.handlers.messages import sender_id
from userbot.modules.capture import repository
from userbot.modules.capture.chat_meta import meta_from_chat
from userbot.modules.media import self_destruct_ttl
@PyroClient.on_edited_message()
async def on_edited_message(client: PyroClient, message: Message) -> None:
ctx = client.capture
if ctx is None or message.empty or message.chat is None or message.date is None:
return
chat = message.chat
chat_id = chat.id or 0
meta = meta_from_chat(chat, ctx.contacts.ids)
toggles = ctx.resolve(meta)
if not toggles.track_edits_deletes:
return
await repository.add_version(
ctx.pool,
ctx.account_id,
chat_id,
message.id,
message.date,
sender_id(message),
message.text or message.caption,
str(message),
message.edit_date,
has_media=message.media is not None,
is_self_destruct=self_destruct_ttl(message) is not None,
)
handlers = on_edited_message.handlers
+64
View File
@@ -0,0 +1,64 @@
from pyrogram.types import Message
from userbot import PyroClient
from userbot.modules.capture import repository
from userbot.modules.capture.chat_meta import meta_from_chat
from userbot.modules.media import capture_media, self_destruct_ttl
def sender_id(message: Message) -> int | None:
if message.from_user is not None:
return message.from_user.id
if message.sender_chat is not None:
return message.sender_chat.id
return None
def _callbacks(message: Message) -> list[tuple[int, str | None, bytes | None]]:
rows = getattr(message.reply_markup, "inline_keyboard", None)
if not rows:
return []
buttons: list[tuple[int, str | None, bytes | None]] = []
position = 0
for row in rows:
for button in row:
data = button.callback_data
if data is not None:
encoded = data.encode() if isinstance(data, str) else data
buttons.append((position, button.text, encoded))
position += 1
return buttons
@PyroClient.on_message()
async def on_message(client: PyroClient, message: Message) -> None:
ctx = client.capture
if ctx is None or message.empty or message.chat is None or message.date is None:
return
chat = message.chat
chat_id = chat.id or 0
meta = meta_from_chat(chat, ctx.contacts.ids)
toggles = ctx.resolve(meta)
if not toggles.messages:
return
await repository.upsert_message(
ctx.pool,
ctx.account_id,
chat_id,
message.id,
message.date,
sender_id(message),
message.text or message.caption,
str(message),
has_media=message.media is not None,
is_self_destruct=self_destruct_ttl(message) is not None,
)
await capture_media(client, message, ctx, chat_id, message.id, toggles)
buttons = _callbacks(message)
if buttons:
await repository.insert_callbacks(
ctx.pool, ctx.account_id, chat_id, message.id, buttons
)
handlers = on_message.handlers
@@ -0,0 +1,3 @@
from userbot.handlers.raw.dispatcher import handlers
__all__ = ["handlers"]
@@ -0,0 +1,22 @@
from pyrogram import raw
from userbot import PyroClient
HANDLES = (raw.types.UpdateContactsReset, raw.types.UpdateUser)
async def handle(
client: PyroClient, update: raw.base.Update, users: dict, _chats: dict
) -> None:
ctx = client.capture
if ctx is None:
return
if isinstance(update, raw.types.UpdateContactsReset):
await ctx.contacts.refresh()
return
if not isinstance(update, raw.types.UpdateUser):
return
user = users.get(update.user_id)
if not isinstance(user, raw.types.User) or user.min:
return
ctx.contacts.mark(user.id, is_contact=bool(user.contact))
@@ -0,0 +1,19 @@
from pyrogram import raw
from userbot import PyroClient
HANDLES = (
raw.types.UpdateDialogFilter,
raw.types.UpdateDialogFilters,
raw.types.UpdateDialogFilterOrder,
)
async def handle(
client: PyroClient, _update: raw.base.Update, _users: dict, _chats: dict
) -> None:
ctx = client.capture
if ctx is None:
return
await ctx.folders.refresh()
await ctx.reload_policies()
@@ -0,0 +1,25 @@
from collections.abc import Awaitable, Callable
from pyrogram import raw
from userbot import PyroClient
from userbot.handlers.raw import contacts, dialog_filters, reactions
RawHandler = Callable[..., Awaitable[None]]
_MODULES = (contacts, dialog_filters, reactions)
_REGISTRY: dict[type, RawHandler] = {
update_type: module.handle for module in _MODULES for update_type in module.HANDLES
}
@PyroClient.on_raw_update()
async def dispatch(
client: PyroClient, update: raw.base.Update, users: dict, chats: dict
) -> None:
handler = _REGISTRY.get(type(update))
if handler is not None:
await handler(client, update, users, chats)
handlers = dispatch.handlers
@@ -0,0 +1,49 @@
from pyrogram import raw, utils
from userbot import PyroClient
from userbot.modules.capture import repository
from userbot.modules.capture.chat_meta import meta_from_peer
HANDLES = (raw.types.UpdateMessageReactions,)
def _reaction_key(reaction: raw.base.Reaction) -> str | None:
if isinstance(reaction, raw.types.ReactionEmoji):
return reaction.emoticon
if isinstance(reaction, raw.types.ReactionCustomEmoji):
return f"custom:{reaction.document_id}"
return None
def _parse_recent(reactions: raw.base.MessageReactions) -> list[tuple[int, str]]:
recent = getattr(reactions, "recent_reactions", None) or []
parsed: list[tuple[int, str]] = []
for peer_reaction in recent:
key = _reaction_key(peer_reaction.reaction)
if key is None:
continue
try:
peer_id = utils.get_peer_id(peer_reaction.peer_id)
except (ValueError, AttributeError):
continue
parsed.append((peer_id, key))
return parsed
async def handle(
client: PyroClient,
update: raw.types.UpdateMessageReactions,
users: dict,
chats: dict,
) -> None:
ctx = client.capture
if ctx is None:
return
meta = meta_from_peer(update.peer, chats, users, ctx.contacts.ids)
toggles = ctx.resolve(meta)
if not toggles.reactions:
return
current = _parse_recent(update.reactions)
await repository.sync_reactions(
ctx.pool, ctx.account_id, meta.chat_id, update.msg_id, current
)