160 lines
6.6 KiB
Python
160 lines
6.6 KiB
Python
"""Inline-button routing (callback_query).
|
|
|
|
Data grammar (≤64 bytes): op:{arg}:{d8} / pk:{d8}:{field}:{idx} / qc:{idx}
|
|
/ ac:{op}[:{i}] / ar:{op} — full state lives in bot_drafts / pending flows.
|
|
"""
|
|
import logging
|
|
|
|
from app import drafts as draft_store
|
|
from app.runtime import LinkedUser
|
|
from app.texts import CANCELLED, UNLINKED, esc
|
|
|
|
from . import quote, supplier, material, project, decisions
|
|
|
|
logger = logging.getLogger("bot.callbacks")
|
|
|
|
|
|
async def handle_callback(ctx, cq: dict) -> None:
|
|
chat_id = ((cq.get("message") or {}).get("chat") or {}).get("id")
|
|
data = cq.get("data") or ""
|
|
if chat_id is None:
|
|
return
|
|
try:
|
|
await _dispatch(ctx, chat_id, data, cq)
|
|
except Exception:
|
|
logger.exception("callback failed: %s", data)
|
|
await ctx.send(chat_id, "خطا در پردازش دکمه؛ دوباره تلاش کن.")
|
|
finally:
|
|
await ctx.bale.answer_callback_query(cq.get("id"), None)
|
|
|
|
|
|
async def _gate(ctx, chat_id: int) -> LinkedUser | None:
|
|
from app.defaults import AUTHORIZED_USERS
|
|
|
|
if chat_id in AUTHORIZED_USERS:
|
|
u = AUTHORIZED_USERS[chat_id]
|
|
return LinkedUser(
|
|
chat_id=chat_id,
|
|
user_id=u.get("user_id"),
|
|
display_name=u.get("display_name"),
|
|
role=u.get("role", "procurement_manager"),
|
|
is_admin=u.get("is_admin", True),
|
|
)
|
|
await ctx.send(chat_id, "دسترسی شما هنوز تعریف نشده است. درخواست شما جهت بررسی ثبت شد.")
|
|
return None
|
|
|
|
|
|
async def _dispatch(ctx, chat_id: int, data: str, cq: dict) -> None:
|
|
user = await _gate(ctx, chat_id)
|
|
if user is None:
|
|
return
|
|
parts = data.split(":")
|
|
|
|
# ---- pending-flow buttons (accept / arrive / quote pick) ---------------
|
|
if parts[0] == "qc" and len(parts) == 2:
|
|
flow = ctx.pending.get(chat_id)
|
|
if flow and flow.kind == "quote_pick":
|
|
await decisions.apply_quote_pick(ctx, chat_id, user, flow, int(parts[1]))
|
|
return
|
|
if parts[0] == "ac":
|
|
await decisions.handle_accept_callback(ctx, chat_id, user, parts)
|
|
return
|
|
if parts[0] == "ar":
|
|
await decisions.handle_arrive_callback(ctx, chat_id, user, parts)
|
|
return
|
|
|
|
# ---- ambiguity sub-messages (Requirement 2.a) -------------------------
|
|
if parts[0] == "amb" and len(parts) >= 4:
|
|
action, prefix, amb_id = parts[1], parts[2], parts[3]
|
|
draft = await draft_store.get_by_prefix(prefix, chat_id)
|
|
if draft is None:
|
|
await ctx.send(chat_id, "این پیشنویس دیگر معتبر نیست.")
|
|
return
|
|
|
|
payload = draft.payload or {}
|
|
ambiguities = payload.get("ambiguities") or []
|
|
target_amb = next((a for a in ambiguities if a.get("id") == amb_id), None)
|
|
if not target_amb:
|
|
return
|
|
|
|
if action == "ok":
|
|
proposed = target_amb.get("proposed_patch") or {}
|
|
draft_store.merge_quote_patch(payload, proposed)
|
|
target_amb["status"] = "applied"
|
|
await quote.resolve_payload(ctx, payload)
|
|
await draft_store.save_payload(draft, mark_bale_synced=True)
|
|
# In-place edit main review card
|
|
await quote.render(ctx, chat_id, draft, fresh=False)
|
|
# Delete ONLY this ambiguity message
|
|
if target_amb.get("message_id"):
|
|
try:
|
|
await ctx.delete(chat_id, target_amb["message_id"])
|
|
except Exception:
|
|
pass
|
|
elif action == "no":
|
|
target_amb["status"] = "rejected"
|
|
await draft_store.save_payload(draft, mark_bale_synced=True)
|
|
# Delete ONLY this ambiguity message
|
|
if target_amb.get("message_id"):
|
|
try:
|
|
await ctx.delete(chat_id, target_amb["message_id"])
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
# ---- pick a candidate for a draft field -------------------------------
|
|
if parts[0] == "pk" and len(parts) == 4:
|
|
draft = await draft_store.get_by_prefix(parts[1], chat_id)
|
|
if draft is None:
|
|
await ctx.send(chat_id, "این پیشنویس دیگر معتبر نیست.")
|
|
return
|
|
await _apply_pick(ctx, chat_id, user, draft, parts[2], int(parts[3]))
|
|
return
|
|
|
|
# ---- draft commit / cancel --------------------------------------------
|
|
op, arg, prefix = parts[0], parts[1], parts[2] if len(parts) > 2 else ""
|
|
if op in ("q", "s", "m", "p") and arg in ("ok", "ok2", "no"):
|
|
kind = {
|
|
"q": draft_store.KIND_QUOTE,
|
|
"s": draft_store.KIND_SUPPLIER,
|
|
"m": draft_store.KIND_MATERIAL,
|
|
"p": draft_store.KIND_PROJECT,
|
|
}[op]
|
|
draft = await draft_store.get_by_prefix(prefix, chat_id)
|
|
if draft is None or draft.kind != kind:
|
|
await ctx.send(chat_id, "این پیشنویس دیگر معتبر نیست.")
|
|
return
|
|
if arg == "no":
|
|
await draft_store.set_status(draft.id, "cancelled")
|
|
ctx.pending.pop(chat_id, None)
|
|
if draft.review_message_id:
|
|
try:
|
|
await ctx.bale.edit_message_text(chat_id, draft.review_message_id, "❌ این پیشنویس لغو شد.", reply_markup=None)
|
|
await draft_store.set_bale_updated_at(draft.id)
|
|
except Exception:
|
|
pass
|
|
await ctx.send(chat_id, CANCELLED)
|
|
return
|
|
if kind == draft_store.KIND_QUOTE:
|
|
await quote.commit(ctx, chat_id, user, draft, only_resolved=(arg == "ok2"))
|
|
elif kind == draft_store.KIND_SUPPLIER:
|
|
await supplier.commit(ctx, chat_id, user, draft)
|
|
elif kind == draft_store.KIND_MATERIAL:
|
|
await material.commit(ctx, chat_id, user, draft)
|
|
elif kind == draft_store.KIND_PROJECT:
|
|
await project.commit(ctx, chat_id, user, draft)
|
|
|
|
|
|
async def _apply_pick(ctx, chat_id: int, user: LinkedUser, draft, field: str, idx: int) -> None:
|
|
options = (draft.payload.get("options") or {}).get(field) or []
|
|
if idx < 0 or idx >= len(options):
|
|
await ctx.send(chat_id, "گزینه نامعتبر است.")
|
|
return
|
|
choice = options[idx]
|
|
if draft.kind == draft_store.KIND_QUOTE:
|
|
await quote.apply_pick(ctx, chat_id, user, draft, field, choice)
|
|
elif draft.kind == draft_store.KIND_SUPPLIER:
|
|
await supplier.apply_pick(ctx, chat_id, user, draft, field, choice)
|
|
elif draft.kind == draft_store.KIND_MATERIAL:
|
|
await material.apply_pick(ctx, chat_id, user, draft, field, choice)
|