fix(backend): person overview reads live in their own module, no import cycle with read receipts
This commit is contained in:
1 parent
1abb396c36
commit
0dd14c5d5e
4 files changed
+202
-200
No files matched your search
@@ -25,6 +25,7 @@ from utils.read import (
|
|||||||
chats,
|
chats,
|
||||||
media,
|
media,
|
||||||
peers,
|
peers,
|
||||||
|
people,
|
||||||
presence,
|
presence,
|
||||||
presence_stats,
|
presence_stats,
|
||||||
social,
|
social,
|
||||||
@@ -397,8 +398,8 @@ async def get_person(account_id: int, peer_id: int) -> dict[str, Any]:
|
|||||||
of their deleted messages, and the private chat if there is one.
|
of their deleted messages, and the private chat if there is one.
|
||||||
"""
|
"""
|
||||||
pool = await _pool()
|
pool = await _pool()
|
||||||
overview = await peers.get_overview(pool, account_id, peer_id)
|
overview = await people.get_overview(pool, account_id, peer_id)
|
||||||
groups = await peers.get_groups(pool, account_id, peer_id)
|
groups = await people.get_groups(pool, account_id, peer_id)
|
||||||
return {**overview.model_dump(mode="json"), **groups.model_dump(mode="json")}
|
return {**overview.model_dump(mode="json"), **groups.model_dump(mode="json")}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncpg
|
|||||||
from dishka.integrations.fastapi import DishkaRoute, FromDishka
|
from dishka.integrations.fastapi import DishkaRoute, FromDishka
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
|
||||||
from utils.read import peers
|
from utils.read import peers, people
|
||||||
from utils.read.models import (
|
from utils.read.models import (
|
||||||
PeerGroups,
|
PeerGroups,
|
||||||
PeerHistoryView,
|
PeerHistoryView,
|
||||||
@@ -47,18 +47,18 @@ async def peer_history(
|
|||||||
async def peer_overview(
|
async def peer_overview(
|
||||||
pool: FromDishka[asyncpg.Pool], peer_id: int, account_id: AccountId
|
pool: FromDishka[asyncpg.Pool], peer_id: int, account_id: AccountId
|
||||||
) -> PeerOverview:
|
) -> PeerOverview:
|
||||||
return await peers.get_overview(pool, account_id, peer_id)
|
return await people.get_overview(pool, account_id, peer_id)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/peers/{peer_id}/groups")
|
@router.get("/peers/{peer_id}/groups")
|
||||||
async def peer_groups(
|
async def peer_groups(
|
||||||
pool: FromDishka[asyncpg.Pool], peer_id: int, account_id: AccountId
|
pool: FromDishka[asyncpg.Pool], peer_id: int, account_id: AccountId
|
||||||
) -> PeerGroups:
|
) -> PeerGroups:
|
||||||
return await peers.get_groups(pool, account_id, peer_id)
|
return await people.get_groups(pool, account_id, peer_id)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/chats/{chat_id}/changes")
|
@router.get("/chats/{chat_id}/changes")
|
||||||
async def chat_changes(
|
async def chat_changes(
|
||||||
pool: FromDishka[asyncpg.Pool], chat_id: int, account_id: AccountId
|
pool: FromDishka[asyncpg.Pool], chat_id: int, account_id: AccountId
|
||||||
) -> list[ProfileChange]:
|
) -> list[ProfileChange]:
|
||||||
return await peers.get_chat_changes(pool, account_id, chat_id)
|
return await people.get_chat_changes(pool, account_id, chat_id)
|
||||||
@@ -1,23 +1,6 @@
|
|||||||
from collections.abc import Callable
|
|
||||||
from datetime import datetime
|
|
||||||
from itertools import pairwise
|
|
||||||
|
|
||||||
import asyncpg
|
import asyncpg
|
||||||
|
|
||||||
from utils.read import chats
|
from utils.read.models import Page, PeerHistoryView, PeerView, StoryView
|
||||||
from utils.read.avatars import avatar_history
|
|
||||||
from utils.read.models import (
|
|
||||||
DirectChatView,
|
|
||||||
MembershipView,
|
|
||||||
Page,
|
|
||||||
PeerGroups,
|
|
||||||
PeerHistoryView,
|
|
||||||
PeerOverview,
|
|
||||||
PeerView,
|
|
||||||
ProfileChange,
|
|
||||||
SharedGroupView,
|
|
||||||
StoryView,
|
|
||||||
)
|
|
||||||
|
|
||||||
HISTORY_LIMIT = 500
|
HISTORY_LIMIT = 500
|
||||||
|
|
||||||
@@ -45,53 +28,6 @@ ORDER BY observed_at DESC
|
|||||||
LIMIT $3
|
LIMIT $3
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_CHAT_TITLES = """
|
|
||||||
SELECT ts, title, previous, actor_id, message_id
|
|
||||||
FROM (
|
|
||||||
SELECT ts, title,
|
|
||||||
CASE WHEN event = 'title' THEN actor_id END AS actor_id,
|
|
||||||
CASE WHEN event = 'title' THEN message_id END AS message_id,
|
|
||||||
lag(title) OVER (ORDER BY ts, message_id) AS previous
|
|
||||||
FROM chat_history
|
|
||||||
WHERE account_id = $1 AND chat_id = $2 AND event IN ('meta', 'title')
|
|
||||||
AND title IS NOT NULL
|
|
||||||
) titles
|
|
||||||
WHERE previous IS DISTINCT FROM title AND previous IS NOT NULL
|
|
||||||
ORDER BY ts DESC
|
|
||||||
LIMIT $3
|
|
||||||
"""
|
|
||||||
|
|
||||||
_MEMBERSHIPS = """
|
|
||||||
SELECT m.chat_id, m.message_id, m.event, m.actor_id, m.ts,
|
|
||||||
COALESCE(NULLIF(trim(concat_ws(' ', a.first_name, a.last_name)), ''),
|
|
||||||
a.username) AS actor_name
|
|
||||||
FROM memberships m
|
|
||||||
LEFT JOIN peers a ON a.account_id = m.account_id AND a.peer_id = m.actor_id
|
|
||||||
WHERE m.account_id = $1 AND m.user_id = $2
|
|
||||||
ORDER BY m.ts DESC
|
|
||||||
LIMIT 200
|
|
||||||
"""
|
|
||||||
|
|
||||||
_DIRECT_CHAT = """
|
|
||||||
SELECT $2::bigint AS chat_id, COALESCE(cs.message_count, 0) AS message_count,
|
|
||||||
cs.last_date
|
|
||||||
FROM (SELECT 1) one
|
|
||||||
LEFT JOIN chat_stats cs ON cs.account_id = $1 AND cs.chat_id = $2
|
|
||||||
WHERE cs.chat_id IS NOT NULL
|
|
||||||
OR EXISTS (SELECT 1 FROM dialogs WHERE account_id = $1 AND chat_id = $2)
|
|
||||||
"""
|
|
||||||
|
|
||||||
_SENT = """
|
|
||||||
SELECT chat_id, count(*) AS message_count, max(date) AS last_date,
|
|
||||||
max(message_id) AS last_message_id,
|
|
||||||
count(*) FILTER (WHERE deleted_at IS NOT NULL) AS deleted_count
|
|
||||||
FROM messages
|
|
||||||
WHERE account_id = $1 AND sender_id = $2
|
|
||||||
GROUP BY chat_id
|
|
||||||
"""
|
|
||||||
|
|
||||||
type Point = tuple[datetime, str | None]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_peer(
|
async def get_peer(
|
||||||
pool: asyncpg.Pool, account_id: int, peer_id: int
|
pool: asyncpg.Pool, account_id: int, peer_id: int
|
||||||
@@ -126,135 +62,6 @@ async def get_peer_history(
|
|||||||
return [PeerHistoryView(**dict(row)) for row in rows]
|
return [PeerHistoryView(**dict(row)) for row in rows]
|
||||||
|
|
||||||
|
|
||||||
def _full_name(state: PeerHistoryView) -> str | None:
|
|
||||||
parts = [part for part in (state.first_name, state.last_name) if part]
|
|
||||||
return " ".join(parts) or None
|
|
||||||
|
|
||||||
|
|
||||||
def _timeline(
|
|
||||||
states: list[PeerHistoryView], value: Callable[[PeerHistoryView], str | None]
|
|
||||||
) -> list[Point]:
|
|
||||||
points: list[Point] = []
|
|
||||||
for state in states:
|
|
||||||
current = value(state)
|
|
||||||
if not points or points[-1][1] != current:
|
|
||||||
points.append((state.observed_at, current))
|
|
||||||
return points
|
|
||||||
|
|
||||||
|
|
||||||
def _without_gaps(points: list[Point]) -> list[Point]:
|
|
||||||
kept: list[Point] = []
|
|
||||||
for at, current in points:
|
|
||||||
gap = len(kept) > 1 and kept[-1][1] is None and kept[-2][1] == current
|
|
||||||
if gap:
|
|
||||||
kept.pop()
|
|
||||||
elif not kept or kept[-1][1] != current:
|
|
||||||
kept.append((at, current))
|
|
||||||
return kept
|
|
||||||
|
|
||||||
|
|
||||||
def _known(points: list[Point]) -> list[Point]:
|
|
||||||
return _without_gaps([point for point in points if point[1] is not None])
|
|
||||||
|
|
||||||
|
|
||||||
def _changes(field: str, points: list[Point]) -> list[ProfileChange]:
|
|
||||||
return [
|
|
||||||
ProfileChange(field=field, old=old, new=new, changed_at=at)
|
|
||||||
for (_, old), (at, new) in pairwise(points)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def profile_changes(states: list[PeerHistoryView]) -> list[ProfileChange]:
|
|
||||||
ordered = sorted(states, key=lambda state: state.observed_at)
|
|
||||||
alive = [state for state in ordered if not state.is_deleted_account]
|
|
||||||
deleted = _timeline(
|
|
||||||
ordered, lambda state: "deleted" if state.is_deleted_account else None
|
|
||||||
)
|
|
||||||
changes = [
|
|
||||||
*_changes("name", _without_gaps(_timeline(alive, _full_name))),
|
|
||||||
*_changes(
|
|
||||||
"username", _without_gaps(_timeline(alive, lambda state: state.username))
|
|
||||||
),
|
|
||||||
*_changes("phone", _known(_timeline(alive, lambda state: state.phone))),
|
|
||||||
*[change for change in _changes("deleted", deleted) if change.new is not None],
|
|
||||||
]
|
|
||||||
return sorted(changes, key=lambda change: change.changed_at, reverse=True)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_chat_changes(
|
|
||||||
pool: asyncpg.Pool, account_id: int, chat_id: int
|
|
||||||
) -> list[ProfileChange]:
|
|
||||||
rows = await pool.fetch(_CHAT_TITLES, account_id, chat_id, HISTORY_LIMIT)
|
|
||||||
return [
|
|
||||||
ProfileChange(
|
|
||||||
field="title",
|
|
||||||
old=row["previous"],
|
|
||||||
new=row["title"],
|
|
||||||
changed_at=row["ts"],
|
|
||||||
actor_id=row["actor_id"],
|
|
||||||
message_id=row["message_id"],
|
|
||||||
)
|
|
||||||
for row in rows
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def _memberships(
|
|
||||||
pool: asyncpg.Pool, account_id: int, peer_id: int
|
|
||||||
) -> list[MembershipView]:
|
|
||||||
rows = await pool.fetch(_MEMBERSHIPS, account_id, peer_id)
|
|
||||||
ids = list({row["chat_id"] for row in rows})
|
|
||||||
known = {
|
|
||||||
chat.chat_id: chat for chat in await chats.get_chats(pool, account_id, ids)
|
|
||||||
}
|
|
||||||
views: list[MembershipView] = []
|
|
||||||
for row in rows:
|
|
||||||
chat = known.get(row["chat_id"])
|
|
||||||
views.append(
|
|
||||||
MembershipView(
|
|
||||||
**dict(row),
|
|
||||||
chat_title=chat.title if chat else None,
|
|
||||||
has_avatar=bool(chat and chat.has_avatar),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return views
|
|
||||||
|
|
||||||
|
|
||||||
async def get_overview(
|
|
||||||
pool: asyncpg.Pool, account_id: int, peer_id: int
|
|
||||||
) -> PeerOverview:
|
|
||||||
direct = await pool.fetchrow(_DIRECT_CHAT, account_id, peer_id)
|
|
||||||
return PeerOverview(
|
|
||||||
peer=await get_peer(pool, account_id, peer_id),
|
|
||||||
changes=profile_changes(await get_peer_history(pool, account_id, peer_id)),
|
|
||||||
avatars=await avatar_history(pool, account_id, peer_id),
|
|
||||||
memberships=await _memberships(pool, account_id, peer_id),
|
|
||||||
dm=DirectChatView(**dict(direct)) if direct else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_groups(pool: asyncpg.Pool, account_id: int, peer_id: int) -> PeerGroups:
|
|
||||||
async with pool.acquire() as conn, conn.transaction():
|
|
||||||
await conn.execute("SET LOCAL jit = off")
|
|
||||||
rows = await conn.fetch(_SENT, account_id, peer_id)
|
|
||||||
sent = {row["chat_id"]: row for row in rows if row["chat_id"] < 0}
|
|
||||||
found = await chats.get_chats(pool, account_id, list(sent))
|
|
||||||
groups = [
|
|
||||||
SharedGroupView(
|
|
||||||
chat_id=chat.chat_id,
|
|
||||||
title=chat.title,
|
|
||||||
has_avatar=chat.has_avatar,
|
|
||||||
message_count=sent[chat.chat_id]["message_count"],
|
|
||||||
last_date=sent[chat.chat_id]["last_date"],
|
|
||||||
last_message_id=sent[chat.chat_id]["last_message_id"],
|
|
||||||
)
|
|
||||||
for chat in found
|
|
||||||
]
|
|
||||||
groups.sort(key=lambda group: group.last_date, reverse=True)
|
|
||||||
return PeerGroups(
|
|
||||||
groups=groups, deleted_count=sum(row["deleted_count"] for row in rows)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_stories(
|
async def get_stories(
|
||||||
pool: asyncpg.Pool, account_id: int, page: Page, *, peer_id: int | None = None
|
pool: asyncpg.Pool, account_id: int, page: Page, *, peer_id: int | None = None
|
||||||
) -> list[StoryView]:
|
) -> list[StoryView]:
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
|
from datetime import datetime
|
||||||
|
from itertools import pairwise
|
||||||
|
|
||||||
|
import asyncpg
|
||||||
|
|
||||||
|
from utils.read import chats
|
||||||
|
from utils.read.avatars import avatar_history
|
||||||
|
from utils.read.models import (
|
||||||
|
DirectChatView,
|
||||||
|
MembershipView,
|
||||||
|
PeerGroups,
|
||||||
|
PeerHistoryView,
|
||||||
|
PeerOverview,
|
||||||
|
ProfileChange,
|
||||||
|
SharedGroupView,
|
||||||
|
)
|
||||||
|
from utils.read.peers import HISTORY_LIMIT, get_peer, get_peer_history
|
||||||
|
|
||||||
|
_CHAT_TITLES = """
|
||||||
|
SELECT ts, title, previous, actor_id, message_id
|
||||||
|
FROM (
|
||||||
|
SELECT ts, title,
|
||||||
|
CASE WHEN event = 'title' THEN actor_id END AS actor_id,
|
||||||
|
CASE WHEN event = 'title' THEN message_id END AS message_id,
|
||||||
|
lag(title) OVER (ORDER BY ts, message_id) AS previous
|
||||||
|
FROM chat_history
|
||||||
|
WHERE account_id = $1 AND chat_id = $2 AND event IN ('meta', 'title')
|
||||||
|
AND title IS NOT NULL
|
||||||
|
) titles
|
||||||
|
WHERE previous IS DISTINCT FROM title AND previous IS NOT NULL
|
||||||
|
ORDER BY ts DESC
|
||||||
|
LIMIT $3
|
||||||
|
"""
|
||||||
|
|
||||||
|
_MEMBERSHIPS = """
|
||||||
|
SELECT m.chat_id, m.message_id, m.event, m.actor_id, m.ts,
|
||||||
|
COALESCE(NULLIF(trim(concat_ws(' ', a.first_name, a.last_name)), ''),
|
||||||
|
a.username) AS actor_name
|
||||||
|
FROM memberships m
|
||||||
|
LEFT JOIN peers a ON a.account_id = m.account_id AND a.peer_id = m.actor_id
|
||||||
|
WHERE m.account_id = $1 AND m.user_id = $2
|
||||||
|
ORDER BY m.ts DESC
|
||||||
|
LIMIT 200
|
||||||
|
"""
|
||||||
|
|
||||||
|
_DIRECT_CHAT = """
|
||||||
|
SELECT $2::bigint AS chat_id, COALESCE(cs.message_count, 0) AS message_count,
|
||||||
|
cs.last_date
|
||||||
|
FROM (SELECT 1) one
|
||||||
|
LEFT JOIN chat_stats cs ON cs.account_id = $1 AND cs.chat_id = $2
|
||||||
|
WHERE cs.chat_id IS NOT NULL
|
||||||
|
OR EXISTS (SELECT 1 FROM dialogs WHERE account_id = $1 AND chat_id = $2)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_SENT = """
|
||||||
|
SELECT chat_id, count(*) AS message_count, max(date) AS last_date,
|
||||||
|
max(message_id) AS last_message_id,
|
||||||
|
count(*) FILTER (WHERE deleted_at IS NOT NULL) AS deleted_count
|
||||||
|
FROM messages
|
||||||
|
WHERE account_id = $1 AND sender_id = $2
|
||||||
|
GROUP BY chat_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
type Point = tuple[datetime, str | None]
|
||||||
|
|
||||||
|
|
||||||
|
def _full_name(state: PeerHistoryView) -> str | None:
|
||||||
|
parts = [part for part in (state.first_name, state.last_name) if part]
|
||||||
|
return " ".join(parts) or None
|
||||||
|
|
||||||
|
|
||||||
|
def _timeline(
|
||||||
|
states: list[PeerHistoryView], value: Callable[[PeerHistoryView], str | None]
|
||||||
|
) -> list[Point]:
|
||||||
|
points: list[Point] = []
|
||||||
|
for state in states:
|
||||||
|
current = value(state)
|
||||||
|
if not points or points[-1][1] != current:
|
||||||
|
points.append((state.observed_at, current))
|
||||||
|
return points
|
||||||
|
|
||||||
|
|
||||||
|
def _without_gaps(points: list[Point]) -> list[Point]:
|
||||||
|
kept: list[Point] = []
|
||||||
|
for at, current in points:
|
||||||
|
gap = len(kept) > 1 and kept[-1][1] is None and kept[-2][1] == current
|
||||||
|
if gap:
|
||||||
|
kept.pop()
|
||||||
|
elif not kept or kept[-1][1] != current:
|
||||||
|
kept.append((at, current))
|
||||||
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def _known(points: list[Point]) -> list[Point]:
|
||||||
|
return _without_gaps([point for point in points if point[1] is not None])
|
||||||
|
|
||||||
|
|
||||||
|
def _changes(field: str, points: list[Point]) -> list[ProfileChange]:
|
||||||
|
return [
|
||||||
|
ProfileChange(field=field, old=old, new=new, changed_at=at)
|
||||||
|
for (_, old), (at, new) in pairwise(points)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def profile_changes(states: list[PeerHistoryView]) -> list[ProfileChange]:
|
||||||
|
ordered = sorted(states, key=lambda state: state.observed_at)
|
||||||
|
alive = [state for state in ordered if not state.is_deleted_account]
|
||||||
|
deleted = _timeline(
|
||||||
|
ordered, lambda state: "deleted" if state.is_deleted_account else None
|
||||||
|
)
|
||||||
|
changes = [
|
||||||
|
*_changes("name", _without_gaps(_timeline(alive, _full_name))),
|
||||||
|
*_changes(
|
||||||
|
"username", _without_gaps(_timeline(alive, lambda state: state.username))
|
||||||
|
),
|
||||||
|
*_changes("phone", _known(_timeline(alive, lambda state: state.phone))),
|
||||||
|
*[change for change in _changes("deleted", deleted) if change.new is not None],
|
||||||
|
]
|
||||||
|
return sorted(changes, key=lambda change: change.changed_at, reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_chat_changes(
|
||||||
|
pool: asyncpg.Pool, account_id: int, chat_id: int
|
||||||
|
) -> list[ProfileChange]:
|
||||||
|
rows = await pool.fetch(_CHAT_TITLES, account_id, chat_id, HISTORY_LIMIT)
|
||||||
|
return [
|
||||||
|
ProfileChange(
|
||||||
|
field="title",
|
||||||
|
old=row["previous"],
|
||||||
|
new=row["title"],
|
||||||
|
changed_at=row["ts"],
|
||||||
|
actor_id=row["actor_id"],
|
||||||
|
message_id=row["message_id"],
|
||||||
|
)
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def _memberships(
|
||||||
|
pool: asyncpg.Pool, account_id: int, peer_id: int
|
||||||
|
) -> list[MembershipView]:
|
||||||
|
rows = await pool.fetch(_MEMBERSHIPS, account_id, peer_id)
|
||||||
|
ids = list({row["chat_id"] for row in rows})
|
||||||
|
known = {
|
||||||
|
chat.chat_id: chat for chat in await chats.get_chats(pool, account_id, ids)
|
||||||
|
}
|
||||||
|
views: list[MembershipView] = []
|
||||||
|
for row in rows:
|
||||||
|
chat = known.get(row["chat_id"])
|
||||||
|
views.append(
|
||||||
|
MembershipView(
|
||||||
|
**dict(row),
|
||||||
|
chat_title=chat.title if chat else None,
|
||||||
|
has_avatar=bool(chat and chat.has_avatar),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return views
|
||||||
|
|
||||||
|
|
||||||
|
async def get_overview(
|
||||||
|
pool: asyncpg.Pool, account_id: int, peer_id: int
|
||||||
|
) -> PeerOverview:
|
||||||
|
direct = await pool.fetchrow(_DIRECT_CHAT, account_id, peer_id)
|
||||||
|
return PeerOverview(
|
||||||
|
peer=await get_peer(pool, account_id, peer_id),
|
||||||
|
changes=profile_changes(await get_peer_history(pool, account_id, peer_id)),
|
||||||
|
avatars=await avatar_history(pool, account_id, peer_id),
|
||||||
|
memberships=await _memberships(pool, account_id, peer_id),
|
||||||
|
dm=DirectChatView(**dict(direct)) if direct else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_groups(pool: asyncpg.Pool, account_id: int, peer_id: int) -> PeerGroups:
|
||||||
|
async with pool.acquire() as conn, conn.transaction():
|
||||||
|
await conn.execute("SET LOCAL jit = off")
|
||||||
|
rows = await conn.fetch(_SENT, account_id, peer_id)
|
||||||
|
sent = {row["chat_id"]: row for row in rows if row["chat_id"] < 0}
|
||||||
|
found = await chats.get_chats(pool, account_id, list(sent))
|
||||||
|
groups = [
|
||||||
|
SharedGroupView(
|
||||||
|
chat_id=chat.chat_id,
|
||||||
|
title=chat.title,
|
||||||
|
has_avatar=chat.has_avatar,
|
||||||
|
message_count=sent[chat.chat_id]["message_count"],
|
||||||
|
last_date=sent[chat.chat_id]["last_date"],
|
||||||
|
last_message_id=sent[chat.chat_id]["last_message_id"],
|
||||||
|
)
|
||||||
|
for chat in found
|
||||||
|
]
|
||||||
|
groups.sort(key=lambda group: group.last_date, reverse=True)
|
||||||
|
return PeerGroups(
|
||||||
|
groups=groups, deleted_count=sum(row["deleted_count"] for row in rows)
|
||||||
|
)
|
||||||
Reference in new issue
Block a user