feat(api,userbot,frontend): search peers without chats and start tracking them
This commit is contained in:
@@ -16,3 +16,22 @@ DEFAULTS: dict[ChatKind, CaptureToggles] = {
|
||||
backfill=True,
|
||||
),
|
||||
}
|
||||
|
||||
TRACKING: dict[ChatKind, CaptureToggles] = {
|
||||
ChatKind.CHANNEL: CaptureToggles(
|
||||
messages=True,
|
||||
media=True,
|
||||
reactions=True,
|
||||
track_edits_deletes=True,
|
||||
backfill=True,
|
||||
),
|
||||
ChatKind.GROUP: CaptureToggles(
|
||||
messages=True,
|
||||
media=True,
|
||||
reactions=True,
|
||||
track_edits_deletes=True,
|
||||
profile_history=True,
|
||||
backfill=True,
|
||||
),
|
||||
ChatKind.DM: DEFAULTS[ChatKind.DM],
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@ async def list_chats(
|
||||
rows = await pool.fetch(
|
||||
"WITH ids AS ("
|
||||
"SELECT DISTINCT chat_id FROM messages WHERE account_id = $1 "
|
||||
"UNION SELECT chat_id FROM dialogs WHERE account_id = $1), "
|
||||
"UNION SELECT chat_id FROM dialogs WHERE account_id = $1 "
|
||||
"UNION SELECT scope_id FROM capture_policy WHERE account_id = $1 "
|
||||
"AND scope_type = 'chat' AND scope_id IS NOT NULL), "
|
||||
"agg AS (SELECT chat_id, count(*) AS message_count, max(date) AS last_date "
|
||||
"FROM messages WHERE account_id = $1 GROUP BY chat_id) "
|
||||
"SELECT ids.chat_id, COALESCE(agg.message_count, 0) AS message_count, "
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import asyncpg
|
||||
|
||||
from utils.policy.models import ChatKind
|
||||
from utils.read.models import DiscoverItem
|
||||
|
||||
_ESCAPE = str.maketrans({"\\": "\\\\", "%": r"\%", "_": r"\_"})
|
||||
|
||||
_IS_BROADCAST = """
|
||||
SELECT COALESCE(raw->'chat'->>'type', raw->>'type') = 'ChatType.CHANNEL'
|
||||
FROM chat_history
|
||||
WHERE account_id = $1 AND chat_id = $2
|
||||
AND COALESCE(raw->'chat'->>'type', raw->>'type') IS NOT NULL
|
||||
ORDER BY ts DESC LIMIT 1
|
||||
"""
|
||||
|
||||
_IS_TRACKED = """
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM capture_policy
|
||||
WHERE account_id = $1 AND scope_type = 'chat' AND scope_id = $2
|
||||
)
|
||||
"""
|
||||
|
||||
_ITEMS = """
|
||||
WITH chat_meta AS (
|
||||
SELECT DISTINCT ON (chat_id) chat_id, title,
|
||||
COALESCE(raw->'chat'->>'username', raw->>'username') AS username,
|
||||
COALESCE(raw->'chat'->>'type', raw->>'type') = 'ChatType.CHANNEL'
|
||||
AS is_broadcast
|
||||
FROM chat_history
|
||||
WHERE account_id = $1 AND title IS NOT NULL
|
||||
ORDER BY chat_id, ts DESC
|
||||
), hits AS (
|
||||
SELECT p.peer_id AS chat_id,
|
||||
COALESCE(NULLIF(concat_ws(' ', p.first_name, p.last_name), ''), p.username)
|
||||
AS title,
|
||||
p.username,
|
||||
COALESCE((p.raw->>'is_bot')::bool, (p.raw->>'bot')::bool, false) AS is_bot,
|
||||
COALESCE((p.raw->>'is_contact')::bool, (p.raw->>'contact')::bool, false)
|
||||
AS is_contact,
|
||||
false AS is_broadcast
|
||||
FROM peers p
|
||||
WHERE p.account_id = $1 AND (p.peer_id = ANY($3::bigint[]) OR ($2 <> '' AND (
|
||||
concat_ws(' ', p.first_name, p.last_name) ILIKE $2
|
||||
OR p.username ILIKE $2 OR p.phone ILIKE $2)))
|
||||
UNION ALL
|
||||
SELECT c.chat_id, c.title, c.username, false, false,
|
||||
COALESCE(c.is_broadcast, false)
|
||||
FROM chat_meta c
|
||||
WHERE c.chat_id = ANY($3::bigint[])
|
||||
OR ($2 <> '' AND (c.title ILIKE $2 OR c.username ILIKE $2))
|
||||
), merged AS (
|
||||
SELECT chat_id, max(title) AS title, max(username) AS username,
|
||||
bool_or(is_bot) AS is_bot, bool_or(is_contact) AS is_contact,
|
||||
bool_or(is_broadcast) AS is_broadcast
|
||||
FROM hits GROUP BY chat_id
|
||||
), counts AS (
|
||||
SELECT chat_id, count(*) AS message_count FROM messages
|
||||
WHERE account_id = $1 AND chat_id IN (SELECT chat_id FROM merged)
|
||||
GROUP BY chat_id
|
||||
)
|
||||
SELECT m.chat_id, m.title, m.username, m.is_bot, m.is_contact, m.is_broadcast,
|
||||
COALESCE(c.message_count, 0) AS message_count,
|
||||
EXISTS (SELECT 1 FROM avatars a
|
||||
WHERE a.account_id = $1 AND a.owner_id = m.chat_id) AS has_avatar,
|
||||
EXISTS (SELECT 1 FROM dialogs d
|
||||
WHERE d.account_id = $1 AND d.chat_id = m.chat_id) AS in_dialogs,
|
||||
EXISTS (SELECT 1 FROM capture_policy cp WHERE cp.account_id = $1
|
||||
AND cp.scope_type = 'chat' AND cp.scope_id = m.chat_id) AS tracked
|
||||
FROM merged m LEFT JOIN counts c ON c.chat_id = m.chat_id
|
||||
ORDER BY in_dialogs DESC, message_count DESC, is_contact DESC, m.title
|
||||
LIMIT $4
|
||||
"""
|
||||
|
||||
|
||||
def _kind(chat_id: int, *, is_broadcast: bool) -> str:
|
||||
if chat_id > 0:
|
||||
return "private"
|
||||
return "channel" if is_broadcast else "group"
|
||||
|
||||
|
||||
def _to_item(row: asyncpg.Record) -> DiscoverItem:
|
||||
return DiscoverItem(
|
||||
chat_id=row["chat_id"],
|
||||
title=row["title"],
|
||||
username=row["username"],
|
||||
kind=_kind(row["chat_id"], is_broadcast=row["is_broadcast"]),
|
||||
is_bot=row["is_bot"],
|
||||
is_contact=row["is_contact"],
|
||||
has_avatar=row["has_avatar"],
|
||||
message_count=row["message_count"],
|
||||
in_dialogs=row["in_dialogs"],
|
||||
tracked=row["tracked"],
|
||||
)
|
||||
|
||||
|
||||
async def search(
|
||||
pool: asyncpg.Pool, account_id: int, query: str, limit: int
|
||||
) -> list[DiscoverItem]:
|
||||
text = query.strip()
|
||||
if not text:
|
||||
return []
|
||||
rows = await pool.fetch(
|
||||
_ITEMS, account_id, f"%{text.translate(_ESCAPE)}%", [], limit
|
||||
)
|
||||
return [_to_item(row) for row in rows]
|
||||
|
||||
|
||||
async def by_ids(
|
||||
pool: asyncpg.Pool, account_id: int, ids: list[int]
|
||||
) -> list[DiscoverItem]:
|
||||
if not ids:
|
||||
return []
|
||||
rows = await pool.fetch(_ITEMS, account_id, "", ids, len(ids))
|
||||
by_id = {row["chat_id"]: _to_item(row) for row in rows}
|
||||
return [by_id[chat_id] for chat_id in ids if chat_id in by_id]
|
||||
|
||||
|
||||
async def get_item(pool: asyncpg.Pool, account_id: int, chat_id: int) -> DiscoverItem:
|
||||
known = await by_ids(pool, account_id, [chat_id])
|
||||
if known:
|
||||
return known[0]
|
||||
kind = await chat_kind(pool, account_id, chat_id)
|
||||
return DiscoverItem(
|
||||
chat_id=chat_id,
|
||||
title=None,
|
||||
username=None,
|
||||
kind="private" if kind is ChatKind.DM else kind.value,
|
||||
is_bot=False,
|
||||
is_contact=False,
|
||||
has_avatar=False,
|
||||
message_count=0,
|
||||
in_dialogs=False,
|
||||
tracked=bool(await pool.fetchval(_IS_TRACKED, account_id, chat_id)),
|
||||
)
|
||||
|
||||
|
||||
async def chat_kind(pool: asyncpg.Pool, account_id: int, chat_id: int) -> ChatKind:
|
||||
if chat_id > 0:
|
||||
return ChatKind.DM
|
||||
is_broadcast = await pool.fetchval(_IS_BROADCAST, account_id, chat_id)
|
||||
return ChatKind.CHANNEL if is_broadcast else ChatKind.GROUP
|
||||
@@ -38,6 +38,19 @@ class ChatListItem(BaseModel):
|
||||
last_sender_id: int | None
|
||||
|
||||
|
||||
class DiscoverItem(BaseModel):
|
||||
chat_id: int
|
||||
title: str | None
|
||||
username: str | None
|
||||
kind: str
|
||||
is_bot: bool
|
||||
is_contact: bool
|
||||
has_avatar: bool
|
||||
message_count: int
|
||||
in_dialogs: bool
|
||||
tracked: bool
|
||||
|
||||
|
||||
class EntityView(BaseModel):
|
||||
type: str
|
||||
offset: int
|
||||
|
||||
Reference in New Issue
Block a user