feat(bot): support concurrent drafts and reply-based edits with error resilience

This commit is contained in:
2026-09-29 00:48:08 +03:30
parent 09245cdc2c
commit a2e7c27e85
10 changed files with 146 additions and 36 deletions
+34 -9
View File
@@ -29,15 +29,16 @@ KIND_MATERIAL = "material"
# Store
# ---------------------------------------------------------------------------
async def create(chat_id: int, kind: str, payload: dict, source: str | None = None) -> BotDraft:
async def create(chat_id: int, kind: str, payload: dict, source: str | None = None,
source_message_id: int | None = None) -> BotDraft:
async with SessionLocal() as session:
# one active draft per (chat, kind)
await session.execute(
update(BotDraft)
.where(BotDraft.chat_id == chat_id, BotDraft.kind == kind, BotDraft.status == "active")
.values(status="superseded")
draft = BotDraft(
chat_id=chat_id,
kind=kind,
payload=payload,
source=source,
source_message_id=source_message_id,
)
draft = BotDraft(chat_id=chat_id, kind=kind, payload=payload, source=source)
session.add(draft)
await session.commit()
await session.refresh(draft)
@@ -53,6 +54,22 @@ async def get_active(chat_id: int, kind: str | None = None) -> BotDraft | None:
return (await session.execute(stmt)).scalar_one_or_none()
async def get_by_message_id(chat_id: int, message_id: int) -> BotDraft | None:
"""Find the active draft associated with a given message_id (review card or user source message)."""
async with SessionLocal() as session:
stmt = (
select(BotDraft)
.where(
BotDraft.chat_id == chat_id,
BotDraft.status == "active",
(BotDraft.review_message_id == message_id) | (BotDraft.source_message_id == message_id),
)
.order_by(BotDraft.updated_at.desc())
.limit(1)
)
return (await session.execute(stmt)).scalar_one_or_none()
async def get_by_prefix(draft_prefix: str, chat_id: int | None = None) -> BotDraft | None:
"""Resolve the short id used in callback_data (first 8 hex of the UUID)."""
async with SessionLocal() as session:
@@ -63,12 +80,20 @@ async def get_by_prefix(draft_prefix: str, chat_id: int | None = None) -> BotDra
async def save_payload(draft: BotDraft, missing: dict | None = None,
review_message_id: int | None = None) -> None:
review_message_id: int | None = None,
source_message_id: int | None = None) -> None:
async with SessionLocal() as session:
values: dict = {
"payload": draft.payload,
"missing": missing,
"review_message_id": review_message_id or draft.review_message_id,
}
if source_message_id:
values["source_message_id"] = source_message_id
await session.execute(
update(BotDraft)
.where(BotDraft.id == draft.id)
.values(payload=draft.payload, missing=missing, review_message_id=review_message_id or draft.review_message_id)
.values(**values)
)
await session.commit()