from datetime import UTC, datetime, timedelta from pyrogram import Client, raw from pyrogram.errors import ( ChannelInvalid, ChannelPrivate, ChatTooBig, FloodPremiumWait, FloodWait, MessageIdInvalid, MessageNotReadYet, MessageTooOld, MsgIdInvalid, MsgTooOld, PeerIdInvalid, RPCError, UserPrivacyRestricted, YourPrivacyRestricted, ) from userbot.modules.jobs.context import JobContext from userbot.modules.jobs.registry import register from userbot.modules.read_receipts.repository import save_fetch, save_reads from utils.jobs import enqueue from utils.logging import logger from utils.policy.models import ChatKind from utils.read.peers import get_peers from utils.read.read_receipts import read_info RETRY = timedelta(seconds=60) LATER = timedelta(days=1) Refusal = tuple[tuple[type[Exception], ...], str, timedelta] REFUSALS: tuple[Refusal, ...] = ( ((MessageNotReadYet,), "not_read_yet", RETRY), ((UserPrivacyRestricted,), "user_privacy", RETRY), ((YourPrivacyRestricted,), "your_privacy", LATER), ((ChatTooBig,), "chat_too_big", LATER), ((MsgTooOld, MessageTooOld), "too_old", LATER), ( ( PeerIdInvalid, ChannelInvalid, ChannelPrivate, MessageIdInvalid, MsgIdInvalid, KeyError, ValueError, ), "unavailable", RETRY, ), ) def _refusal(exc: Exception) -> tuple[str, timedelta]: for kinds, outcome, retry in REFUSALS: if isinstance(exc, kinds): return outcome, retry return "error", RETRY def _at(timestamp: int) -> datetime: return datetime.fromtimestamp(timestamp, UTC) async def ask_telegram( client: Client, chat_id: int, message_id: int, *, group: bool ) -> list[tuple[int, datetime]]: peer = await client.resolve_peer(chat_id) if peer is None: msg = f"unknown peer {chat_id}" raise ValueError(msg) if group: participants = await client.invoke( raw.functions.messages.GetMessageReadParticipants( peer=peer, msg_id=message_id ) ) return [(item.user_id, _at(item.date)) for item in participants] outbox = await client.invoke( raw.functions.messages.GetOutboxReadDate(peer=peer, msg_id=message_id) ) return [(chat_id, _at(outbox.date))] async def _enrich_unknown(ctx: JobContext, chat_id: int, reader_ids: list[int]) -> None: known = { peer.peer_id for peer in await get_peers(ctx.pool, ctx.account_id, reader_ids) } if set(reader_ids) - known: await enqueue(ctx.pool, ctx.account_id, "enrich_chat", {"chat_id": chat_id}) @register("fetch_read_info") async def fetch_read_info(ctx: JobContext) -> None: client = ctx.client if client is None: return chat_id = ctx.job.params["chat_id"] message_id = ctx.job.params["message_id"] info = await read_info(ctx.pool, ctx.account_id, chat_id, message_id) if info is None or not info.can_fetch: return group = info.chat_kind == ChatKind.GROUP try: reads = await ask_telegram(client, chat_id, message_id, group=group) except (FloodWait, FloodPremiumWait) as exc: wait = exc.value if isinstance(exc.value, int) else 0 retry = max(RETRY, timedelta(seconds=wait)) await save_fetch(ctx.pool, ctx.account_id, chat_id, message_id, "flood", retry) return except (RPCError, KeyError, ValueError) as exc: outcome, retry = _refusal(exc) if outcome == "error": logger.warning(f"[yellow]Read info {chat_id}/{message_id}: {exc!r}[/]") await save_fetch(ctx.pool, ctx.account_id, chat_id, message_id, outcome, retry) return source = "participants" if group else "outbox_date" await save_reads(ctx.pool, ctx.account_id, chat_id, message_id, source, reads) await save_fetch(ctx.pool, ctx.account_id, chat_id, message_id, "ok", RETRY) if group and reads: await _enrich_unknown(ctx, chat_id, [reader_id for reader_id, _ in reads])