fix(backend): a re-captured or bot-edited message keeps its earlier text as a version, an older copy never overwrites a newer edit
This commit is contained in:
1 parent
33ceea3dfc
commit
516b8e307f
3 files changed
+170
-91
No files matched your search
@@ -2,10 +2,9 @@ from pyrogram.types import Message
|
|||||||
|
|
||||||
from userbot.modules.capture import repository
|
from userbot.modules.capture import repository
|
||||||
from userbot.modules.capture.chat_meta import meta_from_chat
|
from userbot.modules.capture.chat_meta import meta_from_chat
|
||||||
from userbot.modules.capture.message import sender_id
|
from userbot.modules.capture.message import message_row
|
||||||
from userbot.modules.client import PyroClient
|
from userbot.modules.client import PyroClient
|
||||||
from userbot.modules.media import capture_media, media_unique_id, self_destruct_ttl
|
from userbot.modules.media import capture_media
|
||||||
from userbot.modules.raw_json import raw_json
|
|
||||||
from utils.events import notify_bg_event
|
from utils.events import notify_bg_event
|
||||||
|
|
||||||
|
|
||||||
@@ -27,18 +26,7 @@ async def on_edited_message(client: PyroClient, message: Message) -> None:
|
|||||||
if not toggles.track_edits_deletes:
|
if not toggles.track_edits_deletes:
|
||||||
return
|
return
|
||||||
changed = await repository.add_version(
|
changed = await repository.add_version(
|
||||||
ctx.pool,
|
ctx.pool, ctx.account_id, message_row(message, chat_id, message.date)
|
||||||
ctx.account_id,
|
|
||||||
chat_id,
|
|
||||||
message.id,
|
|
||||||
message.date,
|
|
||||||
sender_id(message),
|
|
||||||
message.text or message.caption,
|
|
||||||
raw_json(message),
|
|
||||||
message.edit_date,
|
|
||||||
media_unique_id(message),
|
|
||||||
has_media=message.media is not None,
|
|
||||||
is_self_destruct=self_destruct_ttl(message) is not None,
|
|
||||||
)
|
)
|
||||||
if not changed:
|
if not changed:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from pyrogram import Client
|
from pyrogram import Client
|
||||||
from pyrogram.types import Message
|
from pyrogram.types import Message
|
||||||
|
|
||||||
from userbot.modules.capture import repository
|
from userbot.modules.capture import repository
|
||||||
from userbot.modules.capture.context import CaptureContext
|
from userbot.modules.capture.context import CaptureContext
|
||||||
|
from userbot.modules.capture.repository import MessageRow
|
||||||
from userbot.modules.links import extract_links
|
from userbot.modules.links import extract_links
|
||||||
from userbot.modules.links import repository as links_repository
|
from userbot.modules.links import repository as links_repository
|
||||||
from userbot.modules.media import capture_media, self_destruct_ttl
|
from userbot.modules.media import capture_media, media_unique_id, self_destruct_ttl
|
||||||
from userbot.modules.raw_json import raw_json
|
from userbot.modules.raw_json import raw_json
|
||||||
from utils.policy.models import CaptureToggles
|
from utils.policy.models import CaptureToggles
|
||||||
|
|
||||||
@@ -34,27 +37,35 @@ def callbacks(message: Message) -> list[tuple[int, str | None, bytes | None]]:
|
|||||||
return buttons
|
return buttons
|
||||||
|
|
||||||
|
|
||||||
|
def message_row(message: Message, chat_id: int, date: datetime) -> MessageRow:
|
||||||
|
return MessageRow(
|
||||||
|
chat_id=chat_id,
|
||||||
|
message_id=message.id,
|
||||||
|
date=date,
|
||||||
|
sender_id=sender_id(message),
|
||||||
|
text=message.text or message.caption,
|
||||||
|
raw=raw_json(message),
|
||||||
|
has_media=message.media is not None,
|
||||||
|
is_self_destruct=self_destruct_ttl(message) is not None,
|
||||||
|
edit_date=message.edit_date,
|
||||||
|
media_unique_id=media_unique_id(message),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def capture_message(
|
async def capture_message(
|
||||||
client: Client, message: Message, ctx: CaptureContext, toggles: CaptureToggles
|
client: Client, message: Message, ctx: CaptureContext, toggles: CaptureToggles
|
||||||
) -> None:
|
) -> None:
|
||||||
if message.empty or message.chat is None or message.date is None:
|
if message.empty or message.chat is None or message.date is None:
|
||||||
return
|
return
|
||||||
chat_id = message.chat.id or 0
|
chat_id = message.chat.id or 0
|
||||||
await repository.upsert_message(
|
current = await repository.upsert_message(
|
||||||
ctx.pool,
|
ctx.pool, ctx.account_id, message_row(message, chat_id, message.date)
|
||||||
ctx.account_id,
|
|
||||||
chat_id,
|
|
||||||
message.id,
|
|
||||||
message.date,
|
|
||||||
sender_id(message),
|
|
||||||
message.text or message.caption,
|
|
||||||
raw_json(message),
|
|
||||||
has_media=message.media is not None,
|
|
||||||
is_self_destruct=self_destruct_ttl(message) is not None,
|
|
||||||
)
|
)
|
||||||
from userbot.modules.capture.identity import capture_identity # noqa: PLC0415
|
from userbot.modules.capture.identity import capture_identity # noqa: PLC0415
|
||||||
|
|
||||||
await capture_identity(message, ctx)
|
await capture_identity(message, ctx)
|
||||||
|
if not current:
|
||||||
|
return
|
||||||
await capture_media(client, message, ctx, chat_id, message.id, toggles)
|
await capture_media(client, message, ctx, chat_id, message.id, toggles)
|
||||||
buttons = callbacks(message)
|
buttons = callbacks(message)
|
||||||
if buttons:
|
if buttons:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import asyncpg
|
import asyncpg
|
||||||
@@ -21,13 +22,19 @@ _TOUCH_EDITED = """
|
|||||||
INSERT INTO messages
|
INSERT INTO messages
|
||||||
(account_id, chat_id, message_id, date, sender_id, text, raw,
|
(account_id, chat_id, message_id, date, sender_id, text, raw,
|
||||||
has_media, is_self_destruct, edited_at)
|
has_media, is_self_destruct, edited_at)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, now())
|
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8, $9, CASE WHEN $10 THEN now() END)
|
||||||
ON CONFLICT (account_id, chat_id, message_id, date) DO UPDATE SET
|
ON CONFLICT (account_id, chat_id, message_id, date) DO UPDATE SET
|
||||||
text = EXCLUDED.text,
|
text = EXCLUDED.text,
|
||||||
raw = EXCLUDED.raw,
|
raw = EXCLUDED.raw,
|
||||||
has_media = EXCLUDED.has_media,
|
has_media = EXCLUDED.has_media,
|
||||||
is_self_destruct = EXCLUDED.is_self_destruct,
|
is_self_destruct = EXCLUDED.is_self_destruct,
|
||||||
edited_at = now()
|
edited_at = coalesce(EXCLUDED.edited_at, messages.edited_at)
|
||||||
|
"""
|
||||||
|
|
||||||
|
_LOCK_MESSAGE = """
|
||||||
|
SELECT pg_advisory_xact_lock(
|
||||||
|
hashtextextended(concat_ws(':', 'message', $1::int, $2::bigint, $3::bigint), 0)
|
||||||
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_FIND_DELETED = """
|
_FIND_DELETED = """
|
||||||
@@ -61,9 +68,32 @@ WHERE m.account_id = $1 AND m.chat_id = $2 AND m.message_id = $3
|
|||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_INSERT_PAST_VERSION = """
|
||||||
|
INSERT INTO message_versions
|
||||||
|
(account_id, chat_id, message_id, observed_at, edit_date, text, raw)
|
||||||
|
SELECT $1::int, $2::bigint, $3::bigint,
|
||||||
|
coalesce(
|
||||||
|
(SELECT min(v.observed_at) - interval '1 microsecond'
|
||||||
|
FROM message_versions v
|
||||||
|
WHERE v.account_id = $1 AND v.chat_id = $2 AND v.message_id = $3
|
||||||
|
AND v.edit_date > $4::timestamptz),
|
||||||
|
$4::timestamptz,
|
||||||
|
$5::timestamptz
|
||||||
|
),
|
||||||
|
$4::timestamptz, $6::text, $7::jsonb
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM message_versions v
|
||||||
|
WHERE v.account_id = $1 AND v.chat_id = $2 AND v.message_id = $3
|
||||||
|
AND v.edit_date IS NOT DISTINCT FROM $4::timestamptz
|
||||||
|
AND v.text IS NOT DISTINCT FROM $6::text
|
||||||
|
)
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
"""
|
||||||
|
|
||||||
_CURRENT_CONTENT = """
|
_CURRENT_CONTENT = """
|
||||||
SELECT
|
SELECT
|
||||||
m.text AS text,
|
m.text AS text,
|
||||||
|
m.raw->>'edit_date' AS edit_date,
|
||||||
(SELECT d.unique_id FROM media d
|
(SELECT d.unique_id FROM media d
|
||||||
WHERE d.account_id = m.account_id AND d.chat_id = m.chat_id
|
WHERE d.account_id = m.account_id AND d.chat_id = m.chat_id
|
||||||
AND d.message_id = m.message_id) AS media_unique_id
|
AND d.message_id = m.message_id) AS media_unique_id
|
||||||
@@ -121,33 +151,108 @@ WHERE evicted_key = $2 OR (unique_id = $1 AND evicted_at IS NOT NULL)
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
async def upsert_message( # noqa: PLR0913
|
@dataclass(frozen=True, slots=True)
|
||||||
pool: asyncpg.Pool,
|
class MessageRow:
|
||||||
account_id: int,
|
chat_id: int
|
||||||
chat_id: int,
|
message_id: int
|
||||||
message_id: int,
|
date: datetime
|
||||||
date: datetime,
|
sender_id: int | None
|
||||||
sender_id: int | None,
|
text: str | None
|
||||||
text: str | None,
|
raw: str
|
||||||
raw: str,
|
has_media: bool
|
||||||
*,
|
is_self_destruct: bool
|
||||||
has_media: bool,
|
edit_date: datetime | None
|
||||||
is_self_destruct: bool,
|
media_unique_id: str | None
|
||||||
) -> None:
|
|
||||||
await pool.execute(
|
@property
|
||||||
_UPSERT_MESSAGE,
|
def columns(self) -> tuple:
|
||||||
account_id,
|
return (
|
||||||
chat_id,
|
self.chat_id,
|
||||||
message_id,
|
self.message_id,
|
||||||
date,
|
self.date,
|
||||||
sender_id,
|
self.sender_id,
|
||||||
text,
|
self.text,
|
||||||
raw,
|
self.raw,
|
||||||
has_media,
|
self.has_media,
|
||||||
is_self_destruct,
|
self.is_self_destruct,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _lock_current(
|
||||||
|
conn: asyncpg.Connection, account_id: int, row: MessageRow
|
||||||
|
) -> asyncpg.Record | None:
|
||||||
|
await conn.execute(_LOCK_MESSAGE, account_id, row.chat_id, row.message_id)
|
||||||
|
return await conn.fetchrow(
|
||||||
|
_CURRENT_CONTENT, account_id, row.chat_id, row.message_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_stale(current: asyncpg.Record, row: MessageRow) -> bool:
|
||||||
|
stored = current["edit_date"]
|
||||||
|
if stored is None:
|
||||||
|
return False
|
||||||
|
if row.edit_date is None:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
return row.edit_date < datetime.fromisoformat(stored)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def _record_past(
|
||||||
|
conn: asyncpg.Connection, account_id: int, row: MessageRow
|
||||||
|
) -> None:
|
||||||
|
await conn.execute(
|
||||||
|
_INSERT_PAST_VERSION,
|
||||||
|
account_id,
|
||||||
|
row.chat_id,
|
||||||
|
row.message_id,
|
||||||
|
row.edit_date,
|
||||||
|
row.date,
|
||||||
|
row.text,
|
||||||
|
row.raw,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _record_edit(
|
||||||
|
conn: asyncpg.Connection, account_id: int, row: MessageRow, *, mark_edited: bool
|
||||||
|
) -> None:
|
||||||
|
await conn.execute(_SNAPSHOT_ORIGINAL, account_id, row.chat_id, row.message_id)
|
||||||
|
await conn.execute(_TOUCH_EDITED, account_id, *row.columns, mark_edited)
|
||||||
|
await conn.execute(
|
||||||
|
_INSERT_VERSION,
|
||||||
|
account_id,
|
||||||
|
row.chat_id,
|
||||||
|
row.message_id,
|
||||||
|
row.edit_date,
|
||||||
|
row.text,
|
||||||
|
row.raw,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def upsert_message(pool: asyncpg.Pool, account_id: int, row: MessageRow) -> bool:
|
||||||
|
async with pool.acquire() as conn, conn.transaction():
|
||||||
|
current = await _lock_current(conn, account_id, row)
|
||||||
|
if current is None:
|
||||||
|
await conn.execute(_UPSERT_MESSAGE, account_id, *row.columns)
|
||||||
|
return True
|
||||||
|
stored_media = current["media_unique_id"]
|
||||||
|
changed = current["text"] != row.text or (
|
||||||
|
stored_media is not None and stored_media != row.media_unique_id
|
||||||
|
)
|
||||||
|
if _is_stale(current, row):
|
||||||
|
if changed:
|
||||||
|
await _record_past(conn, account_id, row)
|
||||||
|
return False
|
||||||
|
if changed:
|
||||||
|
await _record_edit(
|
||||||
|
conn, account_id, row, mark_edited=row.edit_date is not None
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await conn.execute(_UPSERT_MESSAGE, account_id, *row.columns)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def max_message_id(
|
async def max_message_id(
|
||||||
pool: asyncpg.Pool, account_id: int, chat_id: int
|
pool: asyncpg.Pool, account_id: int, chat_id: int
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
@@ -188,45 +293,20 @@ async def mark_deleted_channel(
|
|||||||
await _mark_deleted(pool, account_id, rows)
|
await _mark_deleted(pool, account_id, rows)
|
||||||
|
|
||||||
|
|
||||||
async def add_version( # noqa: PLR0913
|
async def add_version(pool: asyncpg.Pool, account_id: int, row: MessageRow) -> bool:
|
||||||
pool: asyncpg.Pool,
|
|
||||||
account_id: int,
|
|
||||||
chat_id: int,
|
|
||||||
message_id: int,
|
|
||||||
date: datetime,
|
|
||||||
sender_id: int | None,
|
|
||||||
text: str | None,
|
|
||||||
raw: str,
|
|
||||||
edit_date: datetime | None,
|
|
||||||
media_unique_id: str | None,
|
|
||||||
*,
|
|
||||||
has_media: bool,
|
|
||||||
is_self_destruct: bool,
|
|
||||||
) -> bool:
|
|
||||||
async with pool.acquire() as conn, conn.transaction():
|
async with pool.acquire() as conn, conn.transaction():
|
||||||
current = await conn.fetchrow(_CURRENT_CONTENT, account_id, chat_id, message_id)
|
current = await _lock_current(conn, account_id, row)
|
||||||
text_changed = current is None or current["text"] != text
|
if current is not None:
|
||||||
media_changed = (
|
changed = (
|
||||||
current is not None and current["media_unique_id"] != media_unique_id
|
current["text"] != row.text
|
||||||
)
|
or current["media_unique_id"] != row.media_unique_id
|
||||||
if not (text_changed or media_changed):
|
)
|
||||||
return False
|
if not changed:
|
||||||
await conn.execute(_SNAPSHOT_ORIGINAL, account_id, chat_id, message_id)
|
return False
|
||||||
await conn.execute(
|
if _is_stale(current, row):
|
||||||
_TOUCH_EDITED,
|
await _record_past(conn, account_id, row)
|
||||||
account_id,
|
return False
|
||||||
chat_id,
|
await _record_edit(conn, account_id, row, mark_edited=True)
|
||||||
message_id,
|
|
||||||
date,
|
|
||||||
sender_id,
|
|
||||||
text,
|
|
||||||
raw,
|
|
||||||
has_media,
|
|
||||||
is_self_destruct,
|
|
||||||
)
|
|
||||||
await conn.execute(
|
|
||||||
_INSERT_VERSION, account_id, chat_id, message_id, edit_date, text, raw
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user