feat(backend): read time of my messages from the archive, exact dates and group readers asked from telegram on demand

This commit is contained in:
hh committed 2026-10-19 02:40:49 +00:00
1 parent f27943aae6
commit d8dd08d7de
10 files changed
+497 -9

No files matched your search

+18
View File
@@ -23,6 +23,7 @@ from utils.files import resolve_mime
from utils.read import annotations, chats, media, peers, presence, social, watches
from utils.read.accounts import self_user_id, unknown_account_hint
from utils.read.models import DEFAULT_LIMIT, Page
from utils.read.read_receipts import read_info
from utils.search.models import SearchFilters
from utils.search.repository import search_messages
from utils.storage import ContentAddressedStorage
@@ -295,6 +296,23 @@ async def get_callbacks(
return _dump(items)
@mcp.tool
async def get_read_info(
account_id: int, chat_id: int, message_id: int
) -> dict[str, Any] | None:
"""When the user's own message was read, from the archive only.
`observed_at` is when the archive saw the chat read up to this message,
`exact_at` the read date Telegram reported for a private chat, `readers`
who read it in a group and when, `listened_at` when a voice or round
video was played. `outcome` is the last Telegram answer: ok, not_read_yet,
user_privacy (they hide read time), your_privacy (the user hides theirs),
chat_too_big, too_old, unavailable, flood or error.
"""
info = await read_info(await _pool(), account_id, chat_id, message_id)
return info.model_dump(mode="json") if info else None
@mcp.tool
async def presence_history(
account_id: int,
+2
View File
@@ -1,6 +1,7 @@
import asyncio
import json
import logging
from datetime import UTC, datetime
from typing import Any
import asyncpg
@@ -114,6 +115,7 @@ class EventHub:
"type": "receipt",
"chat_id": event["chat_id"],
"read_up_to": event["message_id"],
"read_at": datetime.now(UTC).isoformat(),
}
return None
+35 -2
View File
@@ -2,10 +2,13 @@ from typing import Annotated
import asyncpg
from dishka.integrations.fastapi import DishkaRoute, FromDishka
from fastapi import APIRouter, Query
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from utils.jobs import enqueue
from utils.read import social
from utils.read.models import CallbackView, LinkView, ReactionView
from utils.read.models import CallbackView, LinkView, ReactionView, ReadInfoView
from utils.read.read_receipts import read_info
router = APIRouter(
prefix="/api/messages/{chat_id}/{message_id}",
@@ -16,6 +19,10 @@ router = APIRouter(
AccountId = Annotated[int, Query()]
class RefreshRequest(BaseModel):
account_id: int
@router.get("/callbacks")
async def message_callbacks(
pool: FromDishka[asyncpg.Pool], chat_id: int, message_id: int, account_id: AccountId
@@ -35,3 +42,29 @@ async def message_links(
pool: FromDishka[asyncpg.Pool], chat_id: int, message_id: int, account_id: AccountId
) -> list[LinkView]:
return await social.get_links(pool, account_id, chat_id, message_id)
@router.get("/read")
async def message_read(
pool: FromDishka[asyncpg.Pool], chat_id: int, message_id: int, account_id: AccountId
) -> ReadInfoView:
info = await read_info(pool, account_id, chat_id, message_id)
if info is None:
raise HTTPException(status_code=404, detail="message not found")
return info
@router.post("/read/refresh")
async def refresh_message_read(
pool: FromDishka[asyncpg.Pool], chat_id: int, message_id: int, body: RefreshRequest
) -> dict[str, int | None]:
info = await read_info(pool, body.account_id, chat_id, message_id)
if info is None or not info.can_fetch:
return {"job_id": None}
job_id = await enqueue(
pool,
body.account_id,
"fetch_read_info",
{"chat_id": chat_id, "message_id": message_id},
)
return {"job_id": job_id}