feat: add annotations, user profiles, watchers, stories, search and more

This commit is contained in:
hh
2026-06-01 17:15:09 +02:00
parent ed469ba8dd
commit 2465bcd184
47 changed files with 5009 additions and 242 deletions
+21 -1
View File
@@ -1,6 +1,6 @@
import asyncpg
from utils.read.models import AvatarRef
from utils.read.models import AvatarHistoryView, AvatarRef
_PEER_UNIQUE_ID = """
SELECT photo_unique_id FROM peers
@@ -18,6 +18,12 @@ SELECT unique_id, storage_key, downloaded, mime FROM avatars
WHERE account_id = $1 AND owner_id = $2 AND unique_id = $3
"""
_AVATAR_HISTORY = """
SELECT unique_id, first_seen_at, downloaded FROM avatars
WHERE account_id = $1 AND owner_id = $2
ORDER BY first_seen_at DESC
"""
async def current_avatar(
pool: asyncpg.Pool, account_id: int, owner_kind: str, owner_id: int
@@ -28,3 +34,17 @@ async def current_avatar(
return None
row = await pool.fetchrow(_AVATAR, account_id, owner_id, unique_id)
return AvatarRef(**dict(row)) if row else None
async def avatar_by_unique_id(
pool: asyncpg.Pool, account_id: int, owner_id: int, unique_id: str
) -> AvatarRef | None:
row = await pool.fetchrow(_AVATAR, account_id, owner_id, unique_id)
return AvatarRef(**dict(row)) if row else None
async def avatar_history(
pool: asyncpg.Pool, account_id: int, owner_id: int
) -> list[AvatarHistoryView]:
rows = await pool.fetch(_AVATAR_HISTORY, account_id, owner_id)
return [AvatarHistoryView(**dict(row)) for row in rows]
+21 -7
View File
@@ -118,25 +118,39 @@ async def list_chats(
return items
async def get_chat_history(
async def get_chat_history( # noqa: PLR0913
pool: asyncpg.Pool,
account_id: int,
chat_id: int,
page: Page,
*,
include_deleted: bool = True,
before_id: int | None = None,
after_id: int | None = None,
) -> list[MessageView]:
where = "account_id = $1 AND chat_id = $2"
if not include_deleted:
where += " AND deleted_at IS NULL"
rows = await pool.fetch(
params: list[object] = [account_id, chat_id]
if after_id is not None:
params.append(after_id)
where += f" AND message_id > ${len(params)}"
order = "date ASC, message_id ASC"
elif before_id is not None:
params.append(before_id)
where += f" AND message_id < ${len(params)}"
order = "date DESC, message_id DESC"
else:
order = "date DESC, message_id DESC"
params.append(page.capped_limit)
query = (
f"SELECT {_MESSAGE_COLS} FROM messages WHERE {where} " # noqa: S608
"ORDER BY date DESC, message_id DESC LIMIT $3 OFFSET $4",
account_id,
chat_id,
page.capped_limit,
page.offset,
f"ORDER BY {order} LIMIT ${len(params)}"
)
if before_id is None and after_id is None:
params.append(page.offset)
query += f" OFFSET ${len(params)}"
rows = await pool.fetch(query, *params)
media_by_key = await _media_map(pool, account_id, rows)
parsed = [(row, load_raw(row["raw"])) for row in rows]
views: list[MessageView] = []
+27
View File
@@ -225,6 +225,12 @@ class AvatarRef(BaseModel):
mime: str | None
class AvatarHistoryView(BaseModel):
unique_id: str
first_seen_at: datetime
downloaded: bool
class CustomEmojiRef(BaseModel):
storage_key: str | None
downloaded: bool
@@ -307,6 +313,27 @@ class PeerHistoryView(BaseModel):
is_deleted_account: bool
class ChatLinkView(BaseModel):
message_id: int
date: datetime | None
url: str
kind: str
web_url: str | None
web_title: str | None
web_site_name: str | None
class DayCount(BaseModel):
day: datetime
count: int
outgoing: int
class MessageAt(BaseModel):
message_id: int
date: datetime
class StoryView(BaseModel):
peer_id: int
story_id: int
+14
View File
@@ -68,3 +68,17 @@ async def get_stories(
*params,
)
return [StoryView(**dict(row)) for row in rows]
async def get_story(
pool: asyncpg.Pool, account_id: int, peer_id: int, story_id: int
) -> StoryView | None:
row = await pool.fetchrow(
"SELECT peer_id, story_id, date, expire_date, caption, media_kind, "
"storage_key, downloaded, views, pinned, deleted FROM stories "
"WHERE account_id = $1 AND peer_id = $2 AND story_id = $3",
account_id,
peer_id,
story_id,
)
return StoryView(**dict(row)) if row else None
+76
View File
@@ -0,0 +1,76 @@
from datetime import datetime, timedelta
import asyncpg
from utils.read.accounts import self_user_id
from utils.read.models import ChatLinkView, DayCount, MediaView, MessageAt, Page
_MEDIA_COLS = (
"id, account_id, chat_id, message_id, kind, storage_key, file_size, "
"mime, ttl_seconds, downloaded, extracted_text, created_at"
)
async def chat_media(
pool: asyncpg.Pool, account_id: int, chat_id: int, kinds: list[str], page: Page
) -> list[MediaView]:
rows = await pool.fetch(
f"SELECT {_MEDIA_COLS} FROM media " # noqa: S608
"WHERE account_id = $1 AND chat_id = $2 AND kind = ANY($3) "
"ORDER BY message_id DESC LIMIT $4 OFFSET $5",
account_id,
chat_id,
kinds,
page.capped_limit,
page.offset,
)
return [MediaView(**dict(row)) for row in rows]
async def chat_links(
pool: asyncpg.Pool, account_id: int, chat_id: int, page: Page
) -> list[ChatLinkView]:
rows = await pool.fetch(
"SELECT l.message_id, m.date, l.url, l.kind, l.web_url, "
"l.web_title, l.web_site_name FROM links l "
"LEFT JOIN messages m ON m.account_id = l.account_id "
"AND m.chat_id = l.chat_id AND m.message_id = l.message_id "
"WHERE l.account_id = $1 AND l.chat_id = $2 "
"ORDER BY l.message_id DESC, l.position LIMIT $3 OFFSET $4",
account_id,
chat_id,
page.capped_limit,
page.offset,
)
return [ChatLinkView(**dict(row)) for row in rows]
async def daily_counts(
pool: asyncpg.Pool, account_id: int, chat_id: int
) -> list[DayCount]:
self_id = await self_user_id(pool, account_id)
rows = await pool.fetch(
"SELECT date_trunc('day', date) AS day, count(*) AS count, "
"count(*) FILTER (WHERE sender_id = $3) AS outgoing FROM messages "
"WHERE account_id = $1 AND chat_id = $2 "
"GROUP BY day ORDER BY day",
account_id,
chat_id,
self_id,
)
return [DayCount(**dict(row)) for row in rows]
async def first_message_on_day(
pool: asyncpg.Pool, account_id: int, chat_id: int, day: datetime
) -> MessageAt | None:
row = await pool.fetchrow(
"SELECT message_id, date FROM messages "
"WHERE account_id = $1 AND chat_id = $2 AND date >= $3 AND date < $4 "
"ORDER BY date, message_id LIMIT 1",
account_id,
chat_id,
day,
day + timedelta(days=1),
)
return MessageAt(**dict(row)) if row else None