feat(backend,frontend): feed gets a bots tab so private is people only, a folder's context menu opens the feed of its chats
This commit is contained in:
1 parent
b9e8a96774
commit
c2180e80ad
11 files changed
+128
-16
No files matched your search
@@ -298,10 +298,11 @@ async def get_feed(
|
||||
"""Deleted and edited messages and profile changes across chats, newest first.
|
||||
|
||||
`kind` picks deleted, edited, profile, messages (deleted and edited) or all;
|
||||
`since` keeps items at or after that time; `chat_id` or `chat_kind` (dm,
|
||||
group, channel) narrow it down. Edited items carry `before` and `after`
|
||||
texts. A deleted item with `message_count` above 1 is a bulk deletion in
|
||||
one chat; read its messages with `get_deleted_messages` for that chat.
|
||||
`since` keeps items at or after that time; `chat_id` or `chat_kind` (dm
|
||||
with people, bot, group, channel) narrow it down. Edited items carry
|
||||
`before` and `after` texts. A deleted item with `message_count` above 1 is
|
||||
a bulk deletion in one chat; read its messages with `get_deleted_messages`
|
||||
for that chat.
|
||||
Profile items cover people in private chats or contacts and chats in the
|
||||
dialog list: `detail` maps `name`, `username`, `photo` (unique file ids) or
|
||||
`title` to `[old, new]`, changes a day apart or closer are merged. Pass
|
||||
|
||||
@@ -29,6 +29,7 @@ async def list_feed(
|
||||
kind: Annotated[FeedKind, Query()] = FeedKind.ALL,
|
||||
chat_id: Annotated[int | None, Query()] = None,
|
||||
chat_kind: Annotated[FeedChatKind | None, Query()] = None,
|
||||
folder_id: Annotated[int | None, Query()] = None,
|
||||
since: Annotated[datetime | None, Query()] = None,
|
||||
cursor: Cursor = None,
|
||||
limit: Limit = DEFAULT_LIMIT,
|
||||
@@ -36,7 +37,13 @@ async def list_feed(
|
||||
return await feed.list_feed(
|
||||
pool,
|
||||
account_id,
|
||||
FeedFilter(kind=kind, chat_id=chat_id, chat_kind=chat_kind, since=since),
|
||||
FeedFilter(
|
||||
kind=kind,
|
||||
chat_id=chat_id,
|
||||
chat_kind=chat_kind,
|
||||
folder_id=folder_id,
|
||||
since=since,
|
||||
),
|
||||
limit=limit,
|
||||
cursor=cursor,
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ from enum import StrEnum
|
||||
|
||||
import asyncpg
|
||||
|
||||
from utils.policy import repository as policy_repository
|
||||
from utils.read import chats, peers
|
||||
from utils.read.accounts import self_user_id
|
||||
from utils.read.models import (
|
||||
@@ -45,6 +46,13 @@ LEFT JOIN LATERAL (
|
||||
) p ON true
|
||||
"""
|
||||
|
||||
_IS_BOT = """EXISTS (
|
||||
SELECT 1 FROM peers p
|
||||
WHERE p.account_id = feed_items.account_id AND p.peer_id = feed_items.chat_id
|
||||
AND COALESCE((p.raw->>'is_bot')::bool, (p.raw->>'bot')::bool,
|
||||
p.raw->>'type' = 'ChatType.BOT', false)
|
||||
)"""
|
||||
|
||||
_UNSEEN = """
|
||||
SELECT count(*) FILTER (WHERE kind = 'deleted') AS deleted,
|
||||
count(*) FILTER (WHERE kind = 'edited') AS edited
|
||||
@@ -69,15 +77,25 @@ class FeedKind(StrEnum):
|
||||
|
||||
class FeedChatKind(StrEnum):
|
||||
DM = "dm"
|
||||
BOT = "bot"
|
||||
GROUP = "group"
|
||||
CHANNEL = "channel"
|
||||
|
||||
|
||||
_CHAT_KIND_CLAUSES = {
|
||||
FeedChatKind.DM: f"chat_kind = 'dm' AND NOT {_IS_BOT}",
|
||||
FeedChatKind.BOT: f"chat_kind = 'dm' AND {_IS_BOT}",
|
||||
FeedChatKind.GROUP: "chat_kind = 'group'",
|
||||
FeedChatKind.CHANNEL: "chat_kind = 'channel'",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class FeedFilter:
|
||||
kind: FeedKind = FeedKind.ALL
|
||||
chat_id: int | None = None
|
||||
chat_kind: FeedChatKind | None = None
|
||||
folder_id: int | None = None
|
||||
since: datetime | None = None
|
||||
|
||||
|
||||
@@ -90,18 +108,34 @@ def decode_cursor(cursor: str) -> tuple[datetime, int]:
|
||||
return _EPOCH + int(micros) * _MICROSECOND, int(key)
|
||||
|
||||
|
||||
async def _folder_chat_ids(
|
||||
pool: asyncpg.Pool, account_id: int, folder_id: int | None
|
||||
) -> list[int] | None:
|
||||
if folder_id is None:
|
||||
return None
|
||||
folder = await policy_repository.get_folder(pool, account_id, folder_id)
|
||||
if folder is None:
|
||||
return []
|
||||
return await chats.folder_chat_ids(pool, account_id, folder)
|
||||
|
||||
|
||||
def _where(
|
||||
account_id: int, feed: FeedFilter, cursor: str | None
|
||||
account_id: int,
|
||||
feed: FeedFilter,
|
||||
folder_chat_ids: list[int] | None,
|
||||
cursor: str | None,
|
||||
) -> tuple[str, list[object]]:
|
||||
params: list[object] = [account_id]
|
||||
clauses = ["account_id = $1"]
|
||||
if feed.kind is FeedKind.MESSAGES:
|
||||
clauses.append("kind <> 'profile'")
|
||||
if feed.chat_kind is not None:
|
||||
clauses.append(_CHAT_KIND_CLAUSES[feed.chat_kind])
|
||||
exact = feed.kind not in {FeedKind.ALL, FeedKind.MESSAGES}
|
||||
conditions = (
|
||||
("kind = {}", feed.kind.value if exact else None),
|
||||
("chat_id = {}", feed.chat_id),
|
||||
("chat_kind = {}", feed.chat_kind.value if feed.chat_kind else None),
|
||||
("chat_id = ANY({}::bigint[])", folder_chat_ids),
|
||||
("at >= {}", feed.since),
|
||||
)
|
||||
for template, value in conditions:
|
||||
@@ -191,7 +225,9 @@ async def _build_items(
|
||||
chat=FeedChat(
|
||||
chat_id=row["chat_id"],
|
||||
title=chat.title if chat else None,
|
||||
kind=row["chat_kind"],
|
||||
kind=FeedChatKind.BOT
|
||||
if chat and chat.is_bot and row["chat_kind"] == FeedChatKind.DM
|
||||
else row["chat_kind"],
|
||||
has_avatar=chat.has_avatar if chat else False,
|
||||
),
|
||||
message_id=row["message_id"],
|
||||
@@ -218,7 +254,8 @@ async def list_feed(
|
||||
cursor: str | None = None,
|
||||
) -> FeedPage:
|
||||
size = max(1, min(limit, MAX_LIMIT))
|
||||
where, params = _where(account_id, feed, cursor)
|
||||
folder_chat_ids = await _folder_chat_ids(pool, account_id, feed.folder_id)
|
||||
where, params = _where(account_id, feed, folder_chat_ids, cursor)
|
||||
params.append(size + 1)
|
||||
rows = await pool.fetch(
|
||||
f"SELECT {_ITEM_COLS} FROM feed_items WHERE {where} " # noqa: S608
|
||||
|
||||
Reference in new issue
Block a user