From aa41bbbc7458bfb0ffadaa66c878da9ae59cc468 Mon Sep 17 00:00:00 2001 From: h Date: Mon, 19 Oct 2026 02:23:28 +0000 Subject: [PATCH] fix(backend): group title changes found by enrichment are recorded, enrichment and syncs skip unchanged profiles --- .../src/userbot/modules/capture/identity.py | 6 +-- .../src/userbot/modules/groups/repository.py | 46 +++++++++++++++-- .../modules/jobs/handlers/enrich_chat.py | 51 ++----------------- .../modules/jobs/handlers/sync_contacts.py | 26 +--------- .../modules/jobs/handlers/sync_dialogs.py | 30 ++--------- .../src/userbot/modules/profiles/snapshots.py | 28 +++++++--- 6 files changed, 77 insertions(+), 110 deletions(-) diff --git a/backend/src/userbot/modules/capture/identity.py b/backend/src/userbot/modules/capture/identity.py index 909bf0b..3b27af8 100644 --- a/backend/src/userbot/modules/capture/identity.py +++ b/backend/src/userbot/modules/capture/identity.py @@ -89,17 +89,15 @@ async def _capture_chat(message: Message, ctx: CaptureContext) -> None: meta = (chat.title, photo_unique_id) if not await ctx.chat_meta.changed(ctx.pool, ctx.account_id, chat.id, meta): return - from userbot.modules.groups.repository import insert_chat_history # noqa: PLC0415 + from userbot.modules.groups.repository import record_chat_meta # noqa: PLC0415 - await insert_chat_history( + await record_chat_meta( ctx.pool, ctx.account_id, chat.id, message.id, - "meta", chat.title, photo_unique_id, - None, message.date, raw_json(message), ) diff --git a/backend/src/userbot/modules/groups/repository.py b/backend/src/userbot/modules/groups/repository.py index ac1d3fb..ea02cc7 100644 --- a/backend/src/userbot/modules/groups/repository.py +++ b/backend/src/userbot/modules/groups/repository.py @@ -7,7 +7,23 @@ INSERT INTO chat_history (account_id, chat_id, message_id, event, title, photo_unique_id, actor_id, ts, raw) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb) -ON CONFLICT (account_id, chat_id, message_id) DO NOTHING +ON CONFLICT DO NOTHING +""" + +_INSERT_CHAT_META = """ +INSERT INTO chat_history + (account_id, chat_id, message_id, event, title, photo_unique_id, ts, raw) +SELECT $1, $2, $3, 'meta', $4::text, $5::text, $6, $7::jsonb +WHERE ($4::text, $5::text) IS DISTINCT FROM ( + (SELECT title FROM chat_history + WHERE account_id = $1 AND chat_id = $2 AND event IN ('meta', 'title') + ORDER BY ts DESC LIMIT 1), + (SELECT photo_unique_id FROM chat_history + WHERE account_id = $1 AND chat_id = $2 + AND event IN ('meta', 'photo', 'photo_deleted') + ORDER BY ts DESC LIMIT 1) +) +ON CONFLICT DO NOTHING """ _INSERT_MEMBERSHIP = """ @@ -19,13 +35,14 @@ ON CONFLICT (account_id, chat_id, message_id, user_id) DO NOTHING _LATEST_TITLE = """ SELECT title FROM chat_history -WHERE account_id = $1 AND chat_id = $2 AND title IS NOT NULL +WHERE account_id = $1 AND chat_id = $2 AND event IN ('meta', 'title') ORDER BY ts DESC LIMIT 1 """ _LATEST_PHOTO = """ SELECT photo_unique_id FROM chat_history -WHERE account_id = $1 AND chat_id = $2 AND photo_unique_id IS NOT NULL +WHERE account_id = $1 AND chat_id = $2 + AND event IN ('meta', 'photo', 'photo_deleted') ORDER BY ts DESC LIMIT 1 """ @@ -64,6 +81,29 @@ async def insert_chat_history( # noqa: PLR0913 ) +async def record_chat_meta( # noqa: PLR0913 + pool: asyncpg.Pool, + account_id: int, + chat_id: int, + message_id: int, + title: str | None, + photo_unique_id: str | None, + ts: datetime, + raw: str, +) -> bool: + status = await pool.execute( + _INSERT_CHAT_META, + account_id, + chat_id, + message_id, + title, + photo_unique_id, + ts, + raw, + ) + return status == "INSERT 0 1" + + async def insert_membership( # noqa: PLR0913 pool: asyncpg.Pool, account_id: int, diff --git a/backend/src/userbot/modules/jobs/handlers/enrich_chat.py b/backend/src/userbot/modules/jobs/handlers/enrich_chat.py index affa944..6247075 100644 --- a/backend/src/userbot/modules/jobs/handlers/enrich_chat.py +++ b/backend/src/userbot/modules/jobs/handlers/enrich_chat.py @@ -1,17 +1,11 @@ -from datetime import UTC, datetime - from pyrogram import Client from pyrogram.errors import BadRequest, Forbidden from pyrogram.types import User -from userbot.modules.avatars import note_avatar from userbot.modules.capture.context import CaptureContext -from userbot.modules.groups.repository import insert_chat_history from userbot.modules.jobs.context import JobContext from userbot.modules.jobs.registry import register -from userbot.modules.profiles.parse import snapshot_from_high_level -from userbot.modules.profiles.repository import write_profile -from userbot.modules.raw_json import raw_json +from userbot.modules.profiles.snapshots import save_group, save_user MEMBER_CAP = 200 @@ -25,52 +19,17 @@ LIMIT 100 """ -async def _save_user(ctx: CaptureContext, user: User) -> None: - fields, photo_file_id, photo_unique_id = snapshot_from_high_level(user) - await write_profile( - ctx.pool, - ctx.account_id, - user.id, - fields, - raw_json(user), - history=ctx.resolve_peer(user.id, is_bot=bool(user.is_bot)).profile_history, - ) - if photo_file_id and photo_unique_id: - await note_avatar( - ctx.pool, ctx.account_id, user.id, "peer", photo_unique_id, photo_file_id - ) - - async def _enrich_chat_meta(client: Client, ctx: CaptureContext, chat_id: int) -> None: chat = await client.get_chat(chat_id) - if chat is None: - return - photo = chat.photo - photo_unique_id = photo.big_photo_unique_id if photo else None - photo_file_id = photo.big_file_id if photo else None - await insert_chat_history( - ctx.pool, - ctx.account_id, - chat_id, - 0, - "meta", - chat.title, - photo_unique_id, - None, - datetime.now(UTC), - raw_json(chat), - ) - if photo_file_id and photo_unique_id: - await note_avatar( - ctx.pool, ctx.account_id, chat_id, "chat", photo_unique_id, photo_file_id - ) + if chat is not None: + await save_group(ctx, chat) async def _enrich_members(client: Client, ctx: CaptureContext, chat_id: int) -> None: try: async for member in client.get_chat_members(chat_id, limit=MEMBER_CAP): if isinstance(member.user, User): - await _save_user(ctx, member.user) + await save_user(ctx, member.user) except (BadRequest, Forbidden): return @@ -96,7 +55,7 @@ async def _enrich_senders(client: Client, ctx: CaptureContext, chat_id: int) -> if not ids: return for user in await _fetch_users(client, ids): - await _save_user(ctx, user) + await save_user(ctx, user) @register("enrich_chat") diff --git a/backend/src/userbot/modules/jobs/handlers/sync_contacts.py b/backend/src/userbot/modules/jobs/handlers/sync_contacts.py index 02a401b..0f4e1a9 100644 --- a/backend/src/userbot/modules/jobs/handlers/sync_contacts.py +++ b/backend/src/userbot/modules/jobs/handlers/sync_contacts.py @@ -1,11 +1,8 @@ from pyrogram.types import User -from userbot.modules.avatars import note_avatar from userbot.modules.jobs.context import JobContext from userbot.modules.jobs.registry import register -from userbot.modules.profiles.parse import snapshot_from_high_level -from userbot.modules.profiles.repository import write_profile -from userbot.modules.raw_json import raw_json +from userbot.modules.profiles.snapshots import save_user @register("sync_contacts") @@ -21,26 +18,7 @@ async def sync_contacts(ctx: JobContext) -> None: for user in contacts: if not isinstance(user, User): continue - fields, photo_file_id, photo_unique_id = snapshot_from_high_level(user) - await write_profile( - ctx.pool, - ctx.account_id, - user.id, - fields, - raw_json(user), - history=capture.resolve_peer( - user.id, is_bot=bool(user.is_bot) - ).profile_history, - ) - if photo_file_id and photo_unique_id: - await note_avatar( - ctx.pool, - ctx.account_id, - user.id, - "peer", - photo_unique_id, - photo_file_id, - ) + await save_user(capture, user) processed += 1 await capture.contacts.refresh() await ctx.report_progress({"processed": processed, "done": True}) diff --git a/backend/src/userbot/modules/jobs/handlers/sync_dialogs.py b/backend/src/userbot/modules/jobs/handlers/sync_dialogs.py index f009271..9641d8e 100644 --- a/backend/src/userbot/modules/jobs/handlers/sync_dialogs.py +++ b/backend/src/userbot/modules/jobs/handlers/sync_dialogs.py @@ -2,14 +2,10 @@ from pyrogram import Client from pyrogram.errors import BadRequest, Forbidden from pyrogram.types import User -from userbot.modules.avatars import note_avatar from userbot.modules.capture.context import CaptureContext from userbot.modules.jobs.context import JobContext from userbot.modules.jobs.registry import register -from userbot.modules.profiles.parse import snapshot_from_high_level -from userbot.modules.profiles.repository import write_profile -from userbot.modules.profiles.snapshots import save_group, save_private -from userbot.modules.raw_json import raw_json +from userbot.modules.profiles.snapshots import save_group, save_private, save_user from utils.logging import logger SAVE_EVERY = 100 @@ -30,28 +26,8 @@ async def _enrich_users(client: Client, ctx: CaptureContext, ids: list[int]) -> continue users = result if isinstance(result, list) else [result] for user in users: - if not isinstance(user, User): - continue - fields, photo_file_id, photo_unique_id = snapshot_from_high_level(user) - await write_profile( - ctx.pool, - ctx.account_id, - user.id, - fields, - raw_json(user), - history=ctx.resolve_peer( - user.id, is_bot=bool(user.is_bot) - ).profile_history, - ) - if photo_file_id and photo_unique_id: - await note_avatar( - ctx.pool, - ctx.account_id, - user.id, - "peer", - photo_unique_id, - photo_file_id, - ) + if isinstance(user, User): + await save_user(ctx, user) @register("sync_dialogs") diff --git a/backend/src/userbot/modules/profiles/snapshots.py b/backend/src/userbot/modules/profiles/snapshots.py index 7682493..f0740f5 100644 --- a/backend/src/userbot/modules/profiles/snapshots.py +++ b/backend/src/userbot/modules/profiles/snapshots.py @@ -1,16 +1,34 @@ from datetime import UTC, datetime from pyrogram.enums import ChatType -from pyrogram.types import Chat +from pyrogram.types import Chat, User from userbot.modules.avatars import note_avatar from userbot.modules.capture.context import CaptureContext -from userbot.modules.groups.repository import insert_chat_history -from userbot.modules.profiles.parse import snapshot_from_chat +from userbot.modules.groups.repository import record_chat_meta +from userbot.modules.profiles.parse import snapshot_from_chat, snapshot_from_high_level from userbot.modules.profiles.repository import write_profile from userbot.modules.raw_json import raw_json +async def save_user(ctx: CaptureContext, user: User) -> None: + fields, photo_file_id, photo_unique_id = snapshot_from_high_level(user) + if not await ctx.peer_identity.changed(ctx.pool, ctx.account_id, user.id, fields): + return + await write_profile( + ctx.pool, + ctx.account_id, + user.id, + fields, + raw_json(user), + history=ctx.resolve_peer(user.id, is_bot=bool(user.is_bot)).profile_history, + ) + if photo_file_id and photo_unique_id: + await note_avatar( + ctx.pool, ctx.account_id, user.id, "peer", photo_unique_id, photo_file_id + ) + + async def save_private(ctx: CaptureContext, chat: Chat) -> bool: chat_id = chat.id or 0 fields, photo_file_id, photo_unique_id = snapshot_from_chat(chat) @@ -35,15 +53,13 @@ async def save_group(ctx: CaptureContext, chat: Chat) -> None: photo = chat.photo photo_unique_id = photo.big_photo_unique_id if photo else None photo_file_id = photo.big_file_id if photo else None - await insert_chat_history( + await record_chat_meta( ctx.pool, ctx.account_id, chat_id, 0, - "meta", chat.title, photo_unique_id, - None, datetime.now(UTC), raw_json(chat), )