"""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, 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: link = await ctx.links.get(chat_id) if link and link.user_id: return LinkedUser(chat_id, link.user_id, link.display_name, link.role, is_admin=False) if chat_id in ctx.admin_chat_ids: return LinkedUser(chat_id, None, None, None, is_admin=True) await ctx.send(chat_id, UNLINKED) 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 # ---- 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") and arg in ("ok", "ok2", "no"): kind = {"q": draft_store.KIND_QUOTE, "s": draft_store.KIND_SUPPLIER, "m": draft_store.KIND_MATERIAL}[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) else: await material.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)