
    `gj[                   (   U d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlmZ ddlmZ ddlmZmZmZ 	 ddlmZ dZ	 ddlZdZdd
lmZmZ ddlmZmZm Z m!Z!m"Z" ddl#m$Z$ ddl%m&Z& ddl'm(Z(  ejR                  e*      Z+dZ,dZ-dZ.dZ/dZ0dZ1dZ2dZ3ddddddZ4ddddd dZ5 e
jl                  d!      Z7d"d"d"d#d#d$d%Z8d&e9d'<   d/d(Z:d0d)Z; e e(d*d+            Z<d1d,Z= G d- d.e$e      Z>y# e$ r d	ZdZY w xY w# e$ r d	ZdZY w xY w)2u  
WhatsApp Cloud API adapter — official Meta WhatsApp Business Platform.

This adapter is a *complement* to ``whatsapp.py`` (the Baileys bridge), not
a replacement. The two are independent:

- ``whatsapp.py``      — unofficial Baileys bridge, personal accounts, no
                         public URL needed, account-ban risk.
- ``whatsapp_cloud.py`` (this file) — official Meta Cloud API, Business
                         account required, public webhook URL required,
                         token-based auth.

Both share gating / mention / formatting behavior via ``WhatsAppBehaviorMixin``.

Phase scope (this file evolves across phases):
- Phase 2 — outbound text via Graph API + webhook server with verify-token
            handshake.
- Phase 3 — X-Hub-Signature-256 HMAC verification (raw body, constant-time)
            + wamid replay protection + dispatch via handle_message. Phase 3
            adapter is end-to-end usable for text DMs.
- Phase 4 — media upload + send (image/video/audio/document), inbound
            media download via the Graph media endpoint, voice-note opus
            conversion via ffmpeg with graceful MP3 fallback when ffmpeg
            isn't on PATH. Document text injection for readable types.
- Phase 5 — 24-hour conversation window + template fallback.

Required env vars to enable the adapter:
- WHATSAPP_CLOUD_PHONE_NUMBER_ID  (the Graph URL path component)
- WHATSAPP_CLOUD_ACCESS_TOKEN     (System User permanent token)

Optional / Phase-3+:
- WHATSAPP_CLOUD_APP_ID
- WHATSAPP_CLOUD_APP_SECRET       (HMAC key for X-Hub-Signature-256)
- WHATSAPP_CLOUD_WABA_ID          (analytics / future use)
- WHATSAPP_CLOUD_VERIFY_TOKEN     (hub.verify_token shared secret)
- WHATSAPP_CLOUD_WEBHOOK_HOST     (default 0.0.0.0)
- WHATSAPP_CLOUD_WEBHOOK_PORT     (default 8090)
- WHATSAPP_CLOUD_WEBHOOK_PATH     (default /whatsapp/webhook)
- WHATSAPP_CLOUD_API_VERSION      (default v20.0)
    )annotationsN)OrderedDict)Path)AnyDictOptional)webTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResultSUPPORTED_DOCUMENT_TYPES)WhatsAppBehaviorMixin)rich_sent_store)get_hermes_dirzv20.0z0.0.0.0i  z/whatsapp/webhookzhttps://graph.facebook.comi  0 i  i  i  P i   i  @  )imagevideoaudiodocumentsticker
image/jpegz	video/mp4
audio/mpegapplication/octet-streamz
image/webpffmpeg.oggz.m4az.jpg)z	audio/oggzaudio/x-opus+oggz
audio/opusz	audio/mp4zaudio/x-m4ar   Dict[str, str]"_WHATSAPP_MIME_EXTENSION_OVERRIDESc                   K   	 | j                   j                  |dz          d{   }t        |      |kD  rt        d      |S 7 # t        j                  $ r}|j                  }Y d}~Ad}~ww xY ww)z8Read at most ``max_bytes`` from an aiohttp request body.   Nzpayload too large)contentreadexactlyasyncioIncompleteReadErrorpartiallen
ValueError)request	max_bytesbodyexcs       S/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/whatsapp_cloud.py_read_limited_request_bodyr/      si     __00Q?? 4y9,--K @&& {{s=   A3!A AA A3A A0A+&A3+A00A3c                    | sy| j                  d      d   j                         j                         }t        j	                  |      }|r|S t        j                  |      xs dS )aU  Resolve a mime type to the file extension we want on disk.

    Consults the override map first so types like ``audio/ogg`` produce
    the extension downstream tools actually accept (``.ogg``, not the
    technically-correct-but-broken ``.oga``). Falls back to Python's
    ``mimetypes.guess_extension`` for anything we haven't pinned.
    N;r   )splitstriplowerr    get	mimetypesguess_extension)mimeprimaryoverrides      r.   _ext_for_mimer;      s\     jjoa &&(..0G155g>H$$W-55    zplatforms/whatsapp_cloud/mediazwhatsapp_cloud/mediac                     t         xr t        S )a  Return whether transport dependencies are available.

    aiohttp is needed for the webhook server (inbound). httpx is needed
    for Graph API calls (outbound). Both ship with hermes-agent's default
    dependency set, so this should always be True in normal installs.
    )AIOHTTP_AVAILABLEHTTPX_AVAILABLE r<   r.   !check_whatsapp_cloud_requirementsrA      s     00r<   c                  P    e Zd ZdZdZd1 fdZed2d       Zd3dZed4d       Z	d5dZ
ed6d       Zd7 fd	Zd8 fd
Zd7dZddd9dZd:dZ	 	 d;	 	 	 	 	 	 	 	 	 d<dZd=d>dZ	 d=	 	 	 	 	 	 	 d?dZed@dAd       ZedBdAd       Z	 d=	 	 	 	 	 	 	 	 	 	 	 	 	 dCdZ	 	 	 	 dD	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dEdZ	 d=	 	 	 	 	 	 	 	 	 	 	 	 	 dFdZedGd       ZdHdZ	 d=	 	 	 	 	 	 	 dIdZdddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dJdZddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dKdZ	 	 d;	 	 	 	 	 	 	 	 	 dLd Z	 	 d;	 	 	 	 	 	 	 	 	 dMd!Z	 	 d;	 	 	 	 	 	 	 	 	 dNd"Z 	 	 d;	 	 	 	 	 	 	 	 	 dOd#Z!	 	 	 dP	 	 	 	 	 	 	 	 	 	 	 dQd$Z"dRd%Z#d:d&Z$dd'	 	 	 	 	 dSd(Z%dTd)Z&dTd*Z'dTd+Z(dUd,Z)dVd-Z*dWd.Z+	 	 	 	 	 	 dXd/Z,	 	 	 	 	 	 	 	 dYd0Z- xZ.S )ZWhatsAppCloudAdaptera  WhatsApp Business Cloud API adapter.

    Outbound: HTTPS POST to ``graph.facebook.com/<api_version>/<phone_id>/messages``.
    Inbound: aiohttp server accepting Meta's webhook payloads.

    The mixin must come first in the bases list so its ``format_message``
    overrides ``BasePlatformAdapter.format_message`` (the base provides a
    generic implementation that does not convert markdown to WhatsApp
    syntax). The Baileys adapter does the same.
    Tc                	   t         |   |t        j                         |j                  xs i }t        |j                  dd            j                         | _        t        |j                  dd            j                         | _	        t        |j                  dd            j                         | _
        t        |j                  dd            j                         | _        t        |j                  dd            j                         | _        t        |j                  dd            j                         | _        t        |j                  dt                    | _        t!        |j                  d	t"                    | _        | j'                  |j                  d
t(                    | _        | j'                  |j                  dd            | _        t        |j                  dt.                    | _        dd l}|j                  d      | _        | j7                  | j9                  |j                  d      xs9 |j                  d      xs&  |j:                  d      xs  |j:                  d                  | _        t         |j:                  dd            j                         j?                         dv }|rdn| j<                  rdnd}t        |j                  d      xs*  |j:                  d      xs  |j:                  d      xs |      j                         j?                         | _         t        |j                  d      xs'  |j:                  d      xs  |j:                  dd            j                         j?                         | _!        | j7                  | j9                  |j                  d      xs% |j                  d      xs  |j:                  d                   | _"        | jG                         | _$        tK               | _&        d| _'        d| _(        d| _)        d!| _*        tK               | _+        tK               | _,        tK               | _-        tK               | _.        d | _/        d | _0        y )"Nphone_number_id access_tokenapp_id
app_secretwaba_idverify_tokenwebhook_hostwebhook_portwebhook_pathhealth_pathz/healthapi_versionr   reply_prefix
allow_from	allowFromWHATSAPP_CLOUD_ALLOW_FROMWHATSAPP_CLOUD_ALLOWED_USERSWHATSAPP_CLOUD_ALLOW_ALL_USERS>   1yestrueopen	allowlist	dm_policyWHATSAPP_CLOUD_DM_POLICYWHATSAPP_DM_POLICYgroup_policyWHATSAPP_CLOUD_GROUP_POLICYWHATSAPP_GROUP_POLICYgroup_allow_fromgroupAllowFromWHATSAPP_CLOUD_GROUP_ALLOW_FROMF)1super__init__r
   WHATSAPP_CLOUDextrastrr5   r3   _phone_number_id_access_token_app_id_app_secret_waba_id_verify_tokenDEFAULT_WEBHOOK_HOST_webhook_hostintDEFAULT_WEBHOOK_PORT_webhook_port_normalize_pathDEFAULT_WEBHOOK_PATH_webhook_path_health_pathDEFAULT_API_VERSION_api_versionos_reply_prefix_normalize_allow_ids_coerce_allow_listgetenv_allow_fromr4   
_dm_policy_group_policy_group_allow_from_compile_mention_patterns_mention_patternsr   _seen_wamids_duplicate_count_accepted_count_rejected_signature_count_warned_no_ffmpeg_last_inbound_wamid_by_chat_clarify_state_exec_approval_state_slash_confirm_state_runner_http_client)selfconfigrh   r{   _allow_all_optin_default_dm_policy	__class__s         r.   rf   zWhatsAppCloudAdapter.__init__   s   !8!89" &)3Db)I%J%P%P%R"%eii&C"D"J"J"L  		(B 78>>@ #EIIlB$? @ F F H 9b!9:@@B"%eii&C"D"J"J"L #&eii@T&U"V"%eii@T&U"V"&"6"6IIn&:;#
 "&!5!5IImY/"

 "%UYY}>Q%R!S 	,1IIn,E
 &*%>%>##		,' =99[)=29989= 299;<	&
 BII6;

%'%%'12 'F!%!1!1+v 	  #IIk" "ryy34"ryy-." "	 

 %'%%' 	 #&IIn% :ryy67:ryy0&9#
 %'%%'	 	
 ,0+D+D##		,- @99-.@299>?,
 "&!?!?!A
 7Bm%&$%./& (- EPM(  8C}=H]!=H]! ;?r<   c                t    t        | xs d      j                         xs d}|j                  d      r|S d| S )NrF   /)ri   r3   
startswith)pathraws     r.   ru   z$WhatsAppCloudAdapter._normalize_pathG  s9    $*"o##%,nnS)s83%y8r<   c                x    |j                  d      r|dd }t         d| j                   d| j                   d| S )z<Build a Graph API URL for this adapter's phone-number scope.r   r"   N)r   GRAPH_API_BASErz   rj   )r   r   s     r.   
_graph_urlzWhatsAppCloudAdapter._graph_urlL  sE    ??38D !4#4#4"5Qt7L7L6MQtfUUr<   c                ~    || |<   t        |       t        kD  r&| j                  d       t        |       t        kD  r%yy)z?Insert into a FIFO-capped OrderedDict, evicting oldest entries.FlastN)r(   INTERACTIVE_STATE_CACHE_SIZEpopitem)cachekeyvalues      r.   _bounded_putz!WhatsAppCloudAdapter._bounded_putR  s7     c
%j77MMuM% %j77r<   c                T    | j                   | j                   j                  dd      S y)u   Cloud API has no self-chat concept — never prepend a reply prefix.

        Override the mixin default which keys off WHATSAPP_MODE=self-chat
        (a Baileys-only setting).
        z\n
rF   )r|   replacer   s    r.   _effective_reply_prefixz,WhatsAppCloudAdapter._effective_reply_prefixY  s,     )%%--eT::r<   c                    t               }| D ]C  }|j                  dd      d   }t        j                  dd|      }|j	                  |xs |       E |S )a  Normalize allowlist entries to bare wa_id form.

        The Cloud API identifies users by bare wa_id (digits, no JID
        suffix), while Baileys uses ``<digits>@s.whatsapp.net`` JIDs.
        Users sharing an allowlist between both adapters (or pasting a
        JID/phone number with ``+`` or separators) should still match,
        so strip any ``@...`` suffix and non-digit characters.
        @r"   r   \DrF   )setr2   resubadd)ids
normalizedentrybaredigitss        r.   r}   z)WhatsAppCloudAdapter._normalize_allow_idsc  sY      #u
 	,E;;sA&q)DVVE2t,FNN6?U+	, r<   c                    | j                   dk(  rEt        j                  ddt        |      j	                  dd      d         }|xs || j
                  v S t        |   |      S )z2Allowlist check against the normalized bare wa_id.r[   r   rF   r   r"   r   )r   r   r   ri   r2   r   re   _is_dm_allowed)r   	sender_idr   r   s      r.   r   z#WhatsAppCloudAdapter._is_dm_allowedt  s]    ??k)66%S^%9%9#q%A!%DED%I$*:*:::w%i00r<   c                    t        t        j                  dd            j                         j	                         dv ryt
        |          S )a  Also honor the documented WHATSAPP_CLOUD_ALLOW_ALL_USERS opt-in.

        The shared mixin only checks GATEWAY_ALLOW_ALL_USERS /
        WHATSAPP_ALLOW_ALL_USERS; the Cloud adapter's documented open-access
        opt-in is WHATSAPP_CLOUD_ALLOW_ALL_USERS, so honor it here too.
        rV   rF   >   rW   rX   rY   T)ri   r{   r   r3   r4   re   _open_dm_opted_in)r   r   s    r.   r   z&WhatsAppCloudAdapter._open_dm_opted_in{  sB     ryy92>?EEGMMOSggw(**r<   c                d    t        |xs d      j                         }|sy| j                  |      S )a  Authorize inbound button/list taps before running resolvers.

        Interactive replies bypass the normal ``_build_message_event_from_cloud``
        path (which calls ``_should_process_message``), so approval /
        slash-confirm / clarify taps must re-check DM policy here. Uses the
        strict ``_is_dm_allowed`` gate (not intake/pairing) so a stale prompt
        cannot be answered after the sender is removed from the allowlist.
        rF   F)ri   r3   r   )r   r   	principals      r.   !_is_interactive_sender_authorizedz6WhatsAppCloudAdapter._is_interactive_sender_authorized  s2     	R(..0	""9--r<   F)is_reconnectc                 K   t               s| j                  ddd       y| j                  r| j                  s| j                  ddd       yddlm} t        j                  d	 |       
      | _        t        j                  t              }|j                  j                  | j                  | j                         |j                  j                  | j                   | j"                         |j                  j%                  | j                   | j&                         t        j(                  |      | _        | j*                  j-                          d {    t        j.                  | j*                  | j0                  | j2                        }|j5                          d {    | j7                          t8        j;                  d| j0                  | j2                  | j                   | j<                  | j                         | j>                  st8        jA                  d       | jB                  st8        jA                  d       y7 7 w)Nwhatsapp_cloud_deps_missinguM   aiohttp and httpx are required for whatsapp_cloud — reinstall hermes-agent.F)	retryablewhatsapp_cloud_unconfiguredzLWHATSAPP_CLOUD_PHONE_NUMBER_ID and WHATSAPP_CLOUD_ACCESS_TOKEN are required.r   )platform_httpx_limitsg      >@)timeoutlimits)client_max_sizez=[whatsapp_cloud] Listening on %s:%d%s (Graph %s, phone_id=%s)uq   [whatsapp_cloud] WHATSAPP_CLOUD_VERIFY_TOKEN is not set — the GET subscription handshake will fail until it is.u   [whatsapp_cloud] WHATSAPP_CLOUD_APP_SECRET is not set — incoming webhook POSTs will be refused with 503. Set the app secret to enable inbound message delivery.T)"rA   _set_fatal_errorrj   rk   %gateway.platforms._http_client_limitsr   httpxAsyncClientr   r	   ApplicationWEBHOOK_MAX_BODY_BYTESrouteradd_getrx   _handle_healthrw   _handle_verifyadd_post_handle_webhook	AppRunnerr   setupTCPSiterq   rt   start_mark_connectedloggerinforz   ro   warningrm   )r   r   r   appsites        r.   connectzWhatsAppCloudAdapter.connect  s    02!!-*	 "  $$D,>,>!!- 	 "   	P!--!6!8
 oo.DE

4,,d.A.AB

4--t/B/BC

D..0D0DE}}S)ll  """{{4<<););T=O=OPjjlK!!	
 !!NNH NNE
 1 	#s&   EIIAI$I
%B$I
Ic                  K   | j                   *	 | j                   j                          d {    d | _         | j
                  *	 | j
                  j                          d {    d | _        | j                          y 7 R# t        $ r t        j	                  d       Y pw xY w7 B# t        $ r t        j	                  d       Y `w xY ww)Nz.[whatsapp_cloud] webhook server cleanup failedz)[whatsapp_cloud] http client close failed)r   cleanup	Exceptionr   	exceptionr   aclose_mark_disconnectedr   s    r.   
disconnectzWhatsAppCloudAdapter.disconnect  s     <<#Sll**,,,  DL(N''..000 !%D! - S  !QRS
 1 N  !LMNsm   CB B B CB( #B&$B( (C B B# C"B##C&B( (C	CC		CNc                T  K   | j                   t        dd      S |r|j                         st        dd      S | j                  |      }| j	                  || j                               }| j                  d      }d| j                   d	d
}d}	t        |      D ]  \  }
}dd|d|ddd}|r|
dk(  rd|i|d<   	 | j                   j                  |||       d{   }|j                  dk7  r]	 |j                         }| j#                  ||j                        }t        j%                  d|j                  |       t        d|      c S 	 |j                         }|j'                  d      xs g }|r|d   j'                  d      }	 |	rt)        j*                  ||	|       t        d|	      S 7 # t        $ r7}t        j                  d       t        dt        |            cY d}~c S d}~ww xY w# t        $ r d|j                   dd i}Y w xY w# t        $ r Y w xY ww)u   Send a text message via Graph API.

        ``chat_id`` is the recipient's WhatsApp ID (``wa_id``) — typically
        their phone number with country code, no plus sign.
        NFNot connectedsuccesserrorTr   
message_idmessagesBearer application/jsonAuthorizationzContent-Typewhatsapp
individualtext)r,   preview_url)messaging_productrecipient_typetotyper   r   r   contextheadersjsonz[whatsapp_cloud] send failed   r     z.[whatsapp_cloud] send rejected (status=%d): %sid)r   r   r3   format_messagetruncate_message_outgoing_chunk_limitr   rk   	enumeratepostr   r   r   ri   status_coder   r   _format_graph_errorr   r5   r   record)r   chat_idr#   reply_tometadata	formattedchunksurlr   last_message_ididxchunkpayloadrespr-   r,   	error_msgdatar   s                      r.   sendzWhatsAppCloudAdapter.send  s9     $e?CCgmmodt<<''0	&&y$2L2L2NOooj)&t'9'9&:;.

 *.#F+ &	JC%/".!&t<'G C1H&2H%=	"A!..33Cw3WW
 3&499;D !44T4;K;KL	D$$
 "%yAAyy{hhz*0b&)!fjj&6OI&	\ ""7OYG$?CCK X A  !?@!%s3x@@A ! 4!499Tc?3D4  s   B9H(<!F4F2F4"H(2G7AH(;H
(H(2F44	G4=*G/'G4(H(/G44H(7HH(HH(	H%!H($H%%H(c                X  K   | j                   y| j                  j                  |      }|sy| j                  d      }d| j                   dd}dd|dd	id
}	 | j                   j                  |||       d{   }|j                  dk7  rw	 |j                         }|xs i j                  d      xs i j                  d      }	|	dk(  rt        j                  d|       yt        j                  d|j                  |	       yy7 # t        $ r Y yw xY w# t        $ r d}	Y ]w xY ww)a=  Mark the latest inbound message as read AND show a typing
        indicator in the user's chat UI.

        Best-effort: any error (no inbound wamid yet, network failure,
        stale token, message older than 30 days) is swallowed silently
        so the agent's main reply path isn't blocked by UX polish.
        Nr   r   r   r   r   readr   r   )r   statusr   typing_indicatorr   r   r   codei zS[whatsapp_cloud] typing/read indicator rejected: wamid %s likely older than 30 daysz7[whatsapp_cloud] typing/read indicator returned %d (%s))r   r   r5   r   rk   r  r   r  r   r   r   debug)
r   r
  r  wamidr  r   r  r  r,   r  s
             r.   send_typingz WhatsAppCloudAdapter.send_typingA  sM     $0044W= ooj)&t'9'9&:;.

 ",!' 0	
	**//W7/SSD s"yy{((17R<<VD v~9:?
 M$$d # T 	 	  s`   AD*!D
 <D=D
 D*8D 	?D*D
 
	DD*DD*D'$D*&D''D*c                @  K   | j                   t        dd      S | j                  d      }d| j                   dd}d	d
|d|d}|rd|i|d<   	 | j                   j	                  |||       d{   }|j                  dk7  r[	 |j                         }	| j                  |	|j                        }
t        j                  d|j                  |
       t        d|
      S d}	 |j                         }|j                  d      xs g }|r|d   j                  d      }t        d|      S 7 # t
        $ r5}t        j                  d       t        dt        |            cY d}~S d}~ww xY w# t
        $ r d|j                  dd i}	Y w xY w# t
        $ r Y {w xY ww)u[  Low-level POST for an ``interactive`` message payload.

        ``interactive_body`` is the inner ``interactive: {...}`` dict —
        the caller supplies ``type``, ``body``, and ``action``. This
        wrapper handles auth, error mapping, and message_id extraction so
        each send_* method stays focused on its own button shape.
        NFr   r   r   r   r   r   r   r   interactive)r   r   r   r   r!  r   r   r   z([whatsapp_cloud] interactive send failedr   r   r   z5[whatsapp_cloud] interactive rejected (status=%d): %sr   r  Tr   )r   r   r   rk   r  r   r   r   ri   r  r   r   r  r   r5   )r   r
  interactive_bodyr  r  r   r  r  r-   r,   r  r  r  r   s                 r.   _post_interactivez&WhatsAppCloudAdapter._post_interactive  s     $e?CCooj)&t'9'9&:;.

 ",*!+#
 ".!9GI	=**//W7/SSD
 s"0yy{ 00t7G7GHINNG  ) e9==)-	99;D((:&,"C"%a&**T"2 $?CC3 T 	=GHe3s8<<	=  0tyy#/0  		s   AF!D. 2D,3D. 7FE/ AF$;F F,D. .	E,7*E'!E,"F'E,,F/F	FFF	FFFFc                    t        | xs d      j                         } t        |       |k  r| S | dt        d|dz
         dz   S )zWhatsApp caps quick-reply button titles at 20 chars and list-row
        titles at 24. Truncate with an ellipsis so we surface as much of
        the choice as fits.rF   Nr"   u   …)ri   r3   r(   maxr   limits     r.   _truncate_button_labelz+WhatsAppCloudAdapter._truncate_button_label  sK    
 4:2$$&t9K 'c!UQY'(500r<   c                V    t        | xs d      } t        |       |k  r| S | d|dz
   dz   S )z-``interactive.body.text`` caps at 1024 chars.rF   N   ...)ri   r(   r&  s     r.   _truncate_bodyz#WhatsAppCloudAdapter._truncate_body  s8     4:2t9KKeai 5((r<   c                8  K   | j                   t        dd      S |xs dj                         }|r|xs i j                  d      nd}|s| j	                  |d| |       d{   S |dd	 D cg c]5  }t        |      j                         st        |      j                         7 }	}d
j                  d t        |	      D              }
| j                  d| d|
       }t        |	      dk  rSt        t        |	            D cg c]+  }dd| d| | j                  t        |dz               dd- }}dd|id|id}n~g }t        |	      D ]F  \  }}|j                  d| d| | j                  |dz    d      | j                  |d      d       H |j                  d| dddd       dd|id d!|d"gd#d}| j                  |||       d{   }|j                  r| j                  | j                   ||       |S 7 c c}w c c}w 7 <w)$u  Render a clarify prompt as native WhatsApp interactive buttons.

        - 1–3 choices → ``interactive.type=button`` (inline pill buttons).
        - 4+ choices → ``interactive.type=list`` (tap-to-open sheet with
          up to 10 rows). Telegram's "Other (type answer)" escape hatch
          is appended as the final row, picking it flips the entry into
          text-capture mode handled by the gateway's text intercept.
        - 0 choices (open-ended) → plain text question; the next message
          in the session is captured by the gateway and resolves clarify.

        The button ``id`` field carries ``cl:<clarify_id>:<idx>`` (or
        ``:other``); inbound webhook parsing dispatches on the prefix.
        NFr   r   rF   reply_to_message_idu   ❓ r  
   r   c              3  6   K   | ]  \  }}|d z    d|   yw)r"   z. Nr@   ).0ics      r.   	<genexpr>z4WhatsAppCloudAdapter.send_clarify.<locals>.<genexpr>  s'      !
 $1q1ugRsO!
s   

r*  replycl::r"   r  titler   r7  buttonr   buttonsr   r,   action   )r'  H   )r  r;  descriptionz:otheru   ✏️ OtherzType your own answerlistChooseOptions)r;  rows)r=  sections)r   r   r3   r5   r  ri   joinr  r,  r(   ranger(  appendr#  r   r   r   )r   r
  questionchoices
clarify_idsession_keyr  r  r4  choices_listoption_lines	body_textr  r>  r!  rG  choice_textresults                     r.   send_clarifyz!WhatsAppCloudAdapter.send_clarify  sx    , $e?CCN))+BJHN''(=>PT 7d8*,=QQQ 18O1AAOOyy !
(1,(?!
 
 ''$xj\N(KL	|! !\!23	  $ #J<q6!%!<!<Sq\!J	G 	 !+$g.+K D$-l$;  [
|1SE2!88C!G9R8P#'#>#>{RT#>#U  KKJ<v.'5  +&+4d!C DK --g{X-VV>>d11:{Kq R P	P WsD   A#H%H&H1HH+AH	0H9B"HH0HHc                  K   | j                   t        dd      S ~|xs d}t        |      dk  r|n|dd dz   }	| j                  d|	 d	| |rd
ndz         }
t	        j
                         j                  dd }|r|xs i j                  d      nd}dd|
iddd| dddddd| ddddgid}| j                  |||       d{   }|j                  r| j                  | j                  ||       |S 7 /w)u]  Render a dangerous-command approval prompt with native buttons.

        Two quick-reply buttons (Approve / Deny). Tapping resolves the
        waiting agent via ``tools.approval.resolve_gateway_approval`` —
        same mechanism as the text ``/approve`` flow. The agent thread
        is blocked until the user taps or types a response.
        NFr   r   rF   i   r+  u(   ⚠️ *Command Approval Required*

```
z
```

Reason: z@

Smart DENY: owner override applies to this one operation only.   r.  r=  r   r>  r7  appr:z:approveu   ✅ Approver:  r<  z:denyu   ❌ Denyr?  r/  )r   r   r(   r,  uuiduuid4hexr5   r#  r   r   r   )r   r
  commandrO  rC  r  allow_permanentsmart_deniedcmdcmd_previewrR  approval_idr  r!  rT  s                  r.   send_exec_approvalz'WhatsAppCloudAdapter.send_exec_approval/  sW    " $e?CC m X_c#ds)e2C''= !"m% XdSikm
	 jjl&&s+BJHN''(=>PT Y' '*/}H(EP]!^
 !(*/}E(BZ!X	
" --g{X-VV>>d77kR Ws   CC<
C:0C<c           	     ~  K   | j                   t        dd      S | j                  d| d|       }|r|xs i j                  d      nd}dd	|id
dd| ddddd| ddddd| dddgid}	| j	                  ||	|       d{   }
|
j
                  r| j                  | j                  ||       |
S 7 /w)u5  Render a 3-button slash-command confirmation prompt.

        Mirrors Telegram's send_slash_confirm: Approve Once / Always /
        Cancel. The confirm_id is supplied by the caller (slash command
        handler) — we just store the session_key mapping for the inbound
        resolver to look up.
        NFr   r   *z*

r.  r=  r   r>  r7  zsc:once:u   ✅ Approve Oncer:  r<  z
sc:always:u   🔒 Alwaysz
sc:cancel:u
   ❌ Cancelr?  r/  )r   r   r,  r5   r#  r   r   r   )r   r
  r;  messagerO  
confirm_idr  rR  r  r!  rT  s              r.   send_slash_confirmz'WhatsAppCloudAdapter.send_slash_confirmh  s	      $e?CC''!E7%y(AB	BJHN''(=>PT Y' '*2:,(?J\!]
 !(*4ZL(AM!Z
 !(*4ZL(AL!Y
* --g{X-VV>>d77[Q Ws   B	B=B;0B=c                    | xs i j                  d      xs i }|j                  d      xs | j                  d      xs d}|j                  d      }|d| d| d| S d	| d
| S )Nr   re  r   zunknown errorr  zgraph error z (HTTP z): zHTTP z: )r5   )r,   r  errre  r  s        r.   r  z(WhatsAppCloudAdapter._format_graph_error  s|    zrw'-2 '')$JJ?wwv!$w{m3wiHH{m2gY//r<   c                   K   |ddS w)Ndm)namer   r@   )r   r
  s     r.   get_chat_infoz"WhatsAppCloudAdapter.get_chat_info  s     
  ..s   c           
     z  K   | j                   yt        j                  j                  |      sdd| fS t        j                  j	                  |      }t
        j                  |t
        d         }||kD  r.ddt        j                  j                  |       d| d| d| d		fS |st        j                  |      \  }}|st        j                  |d
      }| j                  d      }dd| j                   i}	 t        |d      5 }	t        j                  j                  |      |	|fdd|fd}
| j                   j                  |||
       d{   }ddd       j&                  dk7  r/	 |j)                         }d| j-                  ||j&                        fS 	 |j)                         }|j                  d      }|sy|dfS 7 s# 1 sw Y   rxY w# t        $ r,}t         j#                  d       dt%        |      fcY d}~S d}~ww xY w# t        $ r d|j*                  dd i}Y w xY w# t        $ r d}Y zw xY ww)u  Upload a local file to the Graph /media endpoint.

        Returns ``(media_id, None)`` on success, ``(None, error_string)``
        on failure. Two-step send: this gets the id, then ``_send_media``
        references it. Used when we have a local file and no public URL.

        ``media_kind`` is one of "image", "video", "audio", "document",
        "sticker" — selects size cap + default mime fallback.
        N)Nr   zFile not found: r   zFile z is z bytes; Cloud API z cap is z bytesr   mediar   r   rb)Nr   )filer   r   )r   filesz$[whatsapp_cloud] media upload failedr   r   r   r  )NzUpload response missing 'id')r   r{   r   existsgetsize_MEDIA_SIZE_LIMITSr5   basenamer6   
guess_type_DEFAULT_MIMEr   rk   rZ   r  r   r   r   ri   r  r   r   r  )r   	file_path
media_kind	mime_typesizecap_r  r   fhrr  r  r-   r,   r  media_ids                   r.   _upload_mediaz"WhatsAppCloudAdapter._upload_media  sU     $(ww~~i(+I;777wwy) $$Z1CJ1OP#:((34D ?'LV= 
 $//	:LIq%))*6PQIoog&"gd.@.@-A$BC
	"i& W"WW--i8"iH);!9-
 "..33CPU3VVW s"0yy{ 11$8H8HIII	99;Dxx~H 7~' WW W  	"CDS>!	"  0tyy#/0  	H	s   C7H;:G A
GGGG H;-H
 =H;!H* =H;GGG 	H!H<H=H;HH;
H'$H;&H''H;*H85H;7H88H;)r  
media_linkcaptionfilenamer  c          
       K   | j                   t        dd      S t        |      t        |      k(  rt        dd      S | j                  d      }d| j                   dd	}	i }
|r||
d
<   n||
d<   |r	|dv r||
d<   |r
|dk(  r||
d<   ddddd|d|||
i}|rd|i|d<   	 | j                   j                  ||	|       d{   }|j                  dk7  r\	 |j                         }| j                  ||j                        }t        j                  d|j                  ||       t        d|      S 	 |j                         }|j                  d      xs g }|r|d   j                  d
      nd}t        d|       S 7 # t        $ r5}t        j                  d       t        dt        |            cY d}~S d}~ww xY w# t        $ r d|j                  dd i}Y w xY w# t        $ r d}Y }w xY ww)!a7  POST a media message referencing either an uploaded media_id or
        a public ``link``.

        Exactly one of ``media_id`` or ``media_link`` must be set. Captions
        and filenames are passed through where Meta accepts them (caption
        on image/video/document; filename on document only).
        NFr   r   z1Exactly one of media_id or media_link must be setr   r   r   r   r  link>   r   r   r   r  r   r  r   r   r   r   r   r   r   r   r   z"[whatsapp_cloud] media send failedr   r   r   z=[whatsapp_cloud] media send rejected (status=%d, kind=%s): %sr   Tr   )r   r   boolr   rk   r  r   r   r   ri   r  r   r   r  r   r5   )r   r
  rz  r  r  r  r  r  r  r   media_blockr  r  r-   r,   r  r  r   r  s                      r.   _send_mediaz WhatsAppCloudAdapter._send_media  s-    $ $e?CC>T*--I 
 ooj)&t'9'9&:;.

 ') (K",Kz%CC%,K	"
j0&.K
#  l'J#
 ".!9GI	=**//W7/SSD
 s"0yy{ 00t7G7GHINNO  *i e9==	99;D((:&,"C(+CFJJt$E $599/ T 	=ABe3s8<<	=  0tyy#/0  	E	s   BG/!E=  E;E= G/F> %AG/1=G .G/;E= =	F;*F60F;1G/6F;;G/>GG/GG/G,)G/+G,,G/)r  r  r  r{  c                 K   |j                  d      r| j                  ||||||       d{   S | j                  |||       d{   \  }}	|	rt        d|	      S | j                  ||||||       d{   S 7 P7 77 w)uX  Smart dispatcher: HTTPS URL → ``link`` send; local path → upload + ``id`` send.

        Prefers the ``link`` path when possible (one fewer Graph round
        trip). Meta fetches from the URL themselves. Used as the common
        backend for ``send_image`` / ``send_video`` / etc. — keeps the
        public method bodies thin.
        zhttp://zhttps://)r  r  r  r  NFr   )r  r  r  r  )r   r  r  r   )
r   r
  sourcerz  r  r  r  r{  r  ri  s
             r.   _send_media_from_path_or_linkz2WhatsAppCloudAdapter._send_media_from_path_or_link8  s     $ 45))!!! *    #00YOO#e377%% & 
 
 	
 P
s3   ,BA?B	B
0B:B;BBBc                J   K   | j                  ||d||       d{   S 7 w)a   Send an image by public URL. Prefers Meta's ``link`` mode.

        ``**kwargs`` absorbs platform-agnostic args the base class passes
        (e.g. ``metadata``) that the Cloud API doesn't have a use for.
        Mirrors send_image_file / send_video / send_voice / send_document.
        r   r  r  Nr  )r   r
  	image_urlr  r  kwargss         r.   
send_imagezWhatsAppCloudAdapter.send_image_  s5      77Y8 8 
 
 	
 
   #!#c                J   K   | j                  ||d||       d{   S 7 w)z1Send a local image file via two-step upload + id.r   r  Nr  )r   r
  
image_pathr  r  r  s         r.   send_image_filez$WhatsAppCloudAdapter.send_image_fileq  5      77Z'H 8 
 
 	
 
r  c                J   K   | j                  ||d||       d{   S 7 w)u=   Send a video. Local path → upload; HTTPS URL → link mode.r   r  Nr  )r   r
  
video_pathr  r  r  s         r.   
send_videozWhatsAppCloudAdapter.send_video~  r  r  c                &  K   |}d}|j                  d       xr@ |j                         j                  d      xr t        j                  j                  |      }|rU| j                  |       d{   }	|	r8	 | j                  ||	d||d       d{   }
	 t        j                  |	       |
S d}| j                  ||d|||       d{   S 7 _7 ?# t        $ r Y |
S w xY w# 	 t        j                  |	       w # t        $ r Y w w xY wxY w7 Bw)a|  Send an audio file as a WhatsApp voice message.

        WhatsApp renders ``audio/ogg; codecs=opus`` as the green
        voice-note bubble; other audio types (MP3, AAC, etc.) appear as
        a generic audio attachment. Hermes TTS produces MP3, so we try
        ffmpeg conversion to opus first and fall back to sending the
        MP3 as-is when ffmpeg is unavailable.
        Nr  z.mp3r   zaudio/ogg; codecs=opus)r  r  r{  r   )
r   r4   endswithr{   r   rs  _convert_to_opusr  unlinkOSError)r   r
  
audio_pathr  r  r  r  r{  is_local_mp3	opus_pathrT  s              r.   
send_voicezWhatsAppCloudAdapter.send_voice  s=      #'	 %%&=>> +  "++F3+z* 	
 "33J??I#'#E#EG '(": $F $ F		),  %I77VWh) 8 
 
 	
+ @ # 		)," 
s   A/D1C2D9C% CC% C .DDDC% 	C"D!C""D%D'C=<D=	D	DD		DDc           	        K   | j                  ||d||xs t        j                  j                  |      |       d{   S 7 w)z<Send a document attachment with optional filename + caption.r   )r  r  r  N)r  r{   r   rv  )r   r
  ry  r  	file_namer  r  s          r.   send_documentz"WhatsAppCloudAdapter.send_document  sN      77Y
="''"2"29"=	 8 
 
 	
 
s   <AAAc                x  K   t         s| j                          y|j                  dd      d   dz   }	 t        j                  t         dd|dd	d
ddddd|t        j
                  j                  t        j
                  j                         d{   }|j                          d{   \  }}|j                  dk7  st        |      j                         s:t        j                  d|j                  |xs dj                  dd      dd        y|S 7 7 m# t        $ r t        j!                  d       Y yw xY ww)a  Convert an MP3 to ``audio/ogg; codecs=opus`` for voice bubbles.

        Returns the path to the converted file, or None if ffmpeg is
        missing / conversion fails (caller falls back to sending the
        original MP3 as an audio file).

        ``-application voip`` tunes the opus encoder for speech.
        ``-b:a 32k -vbr on`` matches the bitrate WhatsApp produces for
        native voice notes (small files, good intelligibility).
        N.r"   r   r   z-yz-iz-c:alibopusz-b:a32kz-vbronz-applicationvoip)stdoutstderrzB[whatsapp_cloud] ffmpeg opus conversion failed (returncode=%s): %sr<   utf-8r   )errorsr   z)[whatsapp_cloud] ffmpeg subprocess raised)_FFMPEG_PATH_warn_once_no_ffmpegrsplitr%   create_subprocess_exec
subprocessDEVNULLPIPEcommunicate
returncoder   rs  r   r   decoder   r   )r   mp3_pathout_pathprocr~  r  s         r.   r  z%WhatsAppCloudAdapter._convert_to_opus  s(     %%'??3*1-6	 77dD(	65&$))11)).. D #..00IAv!#4>+@+@+B*OO]s**79*EdsK	 O! 1  	HI	sZ   0D:AD DD &D'A(D D:D D:D D D74D:6D77D:c                V    | j                   ry d| _         t        j                  d       y )NTu  [whatsapp_cloud] ffmpeg not found on PATH — voice messages will be delivered as MP3 audio attachments instead of native voice notes (green waveform bubble). Install ffmpeg to enable: Windows `winget install Gyan.FFmpeg`, macOS `brew install ffmpeg`, Linux package manager.)r   r   r   r   s    r.   r  z)WhatsAppCloudAdapter._warn_once_no_ffmpeg  s'    !!!%%	
r<   ext_hintc                 K   | j                   yt        |      j                         }t        j                  d|      st
        j                  d|dd        ydd| j                   i}	 | j                   j                  t         d| j                   d| |	       d{   }|j                  dk7  r"t
        j                  d||j                         y	 |j                         }|j                  d      }|j                  d      xs d}|sy	 | j                   j                  ||	       d{   }|j                  dk7  r"t
        j                  d||j                         y|}	|	s|rt        |      }	|	sd}	t         j#                  dd       t         | |	 z  }
	 |
j%                  |j&                         t        |
      |xs dfS 7 -# t        $ r t
        j                  d
|       Y yw xY w# t        $ r Y yw xY w7 # t        $ r t
        j                  d|       Y yw xY w# t(        $ r t
        j                  d|       Y yw xY ww)u  Two-step Graph media download: ``GET /<id>`` → temp URL → bytes.

        Returns ``(local_path, mime_type)`` on success. ``mime_type``
        falls back to what Graph reports in the metadata response.
        Returns ``(None, None)`` on any failure (logged).

        The temporary URL from step 1 is signed and expires in ~5
        minutes; we download immediately and never persist the URL.
        NNNz[A-Za-z0-9._-]+z/[whatsapp_cloud] refusing malformed media id %r@   r   r   r   )r   z4[whatsapp_cloud] media metadata fetch raised (id=%s)r   z?[whatsapp_cloud] media metadata fetch failed (id=%s, status=%d)r  r{  rF   z1[whatsapp_cloud] media bytes fetch raised (id=%s)z<[whatsapp_cloud] media bytes fetch failed (id=%s, status=%d)z.binT)parentsexist_okz5[whatsapp_cloud] failed to write cached media (id=%s))r   ri   r3   r   	fullmatchr   r   rk   r5   r   rz   r   r   r  r   r;   _INBOUND_MEDIA_CACHEmkdirwrite_bytesr#   r  )r   r  r  r   	meta_respmetatemp_urlr8   	blob_respextr  s              r.   _download_media_to_cachez-WhatsAppCloudAdapter._download_media_to_cache  st     $
 x=&&(||.9NNA8CR= "gd.@.@-A$BC		"//33!"!D$5$5#6azB 4  I   C'NNQ)// 	>>#D 88E?xx$*	"//33Hg3NNI   C'NNN)//  t%CC""4$"?'XJse*<<	  !2!23 8}dld**{  	F 		  		 O 	CX 		4  	G 		s   A(I+6G !G"G &1IG6 ()I H 2H3H 7A*I"H, =IG G30I2G33I6	H?IHIH H)&I(H))I,IIIIc                .  K   t        j                  d| j                  j                  | j                  | j
                  t        | j                        t        | j                        t        d u| j                  | j                  | j                  d
      S w)Nok)
r  platformrE   rN   verify_token_configuredapp_secret_configuredffmpeg_presentaccepted
duplicatesrejected_signature)r	   json_responser  r   rj   rw   r  ro   rm   r  r   r   r   )r   r*   s     r.   r   z#WhatsAppCloudAdapter._handle_healthc  s        MM//#'#8#8 $ 2 2+/0B0B+C)-d.>.>)?".d": 00"33&*&D&D
 	
s   BBc                6  K   | j                   st        j                  dd      S |j                  j	                  dd      }|j                  j	                  dd      }|j                  j	                  dd      }|dk7  rt        j                  d	d
      S ddl}|j                  |j                         | j                   j                               st        j                  dd      S |st        j                  d	d      S t        j                  |d      S w)aC  Meta subscription verification handshake.

        Meta calls GET ``<webhook>?hub.mode=subscribe&hub.verify_token=...
        &hub.challenge=...``. We must echo the challenge as plain text iff
        ``hub.mode == "subscribe"`` AND ``hub.verify_token`` matches the
        shared secret. Constant-time comparison.
          zverify_token not configuredr  r   zhub.moderF   zhub.verify_tokenzhub.challenge	subscribe  zbad moder   Ni  zverify_token mismatchzmissing challengez
text/plain)r   content_type)ro   r	   Responsequeryr5   hmaccompare_digestencode)r   r*   modetoken	challenge_hmacs         r.   r   z#WhatsAppCloudAdapter._handle_verifys  s      !! <<s1NOO}}  R0!!"4b9MM%%or:	;<<s<<
 	##ELLND4F4F4M4M4OP<<s1HII<<s1DEE||FFs   DDc                |  K   	 t        |t               d{   }| j                  s,t        j                  d       t        j                  dd      S |j                  j                  d	d
      }| j                  ||      sK| xj                  dz  c_        t        j                  d|t        |             t        j                  d      S ddl}	 |j!                  |      }t#        |t$              st        j                  d      S | j'                  |       d{    t        j                  d      S 7 !# t        $ r t        j                  d      cY S t
        $ r t        j                  d      cY S w xY w# t
        $ r. t        j                  d       t        j                  d      cY S w xY w7 w)u  Inbound webhook POST handler.

        Lifecycle:
          1. Read raw bytes (signature is over the raw body — JSON parsing
             must NOT happen first, or the bytes change).
          2. Verify ``X-Hub-Signature-256`` HMAC against ``app_secret``.
          3. Parse JSON.
          4. Walk ``entry[].changes[].value.{messages, statuses, contacts}``.
          5. Per-message: dedup by wamid, build MessageEvent, dispatch via
             ``handle_message`` (which runs the mixin's gating).
          6. Always respond 200 once we've ack'd a valid request — Meta
             retries on non-200 for up to 7 days, and we don't want to
             multiply downstream agent work because of a transient bug
             during dispatch.
        Ni  )r  r  zr[whatsapp_cloud] webhook POST refused: app_secret unset. Set WHATSAPP_CLOUD_APP_SECRET to enable inbound delivery.r  zapp_secret not configuredr  zX-Hub-Signature-256rF   r"   zW[whatsapp_cloud] rejected webhook: invalid X-Hub-Signature-256 (header=%r, body_len=%d)i  r   z/[whatsapp_cloud] webhook body is not valid JSONr   )r/   r   r)   r	   r  r   rm   r   r   r   r5   _verify_signaturer   r   r(   r   loads
isinstancedict_dispatch_payload)r   r*   r   signature_header_jsonr  s         r.   r   z$WhatsAppCloudAdapter._handle_webhook  s|    $	,2& C LLL <<s1LMM"??../DbI%%c+;<**a/*NN+ C	 <<s++
 		,kk#&G
 '4(<<s++$$W---||3'']  	,<<s++ 	,<<s++	,@  	,NNLM<<s++	, 	.st   F<D; D8D; B5F<F  #:F<F:F<8D; ;E=F<E=:F<<E==F< 4F74F<6F77F<c                   | j                   r|sy|j                  d      sy|t        d      d j                         }|syt	        j
                  | j                   j                  d      |t        j                        j                         }t	        j                  |j                         j                         |j                         j                               S )zVerify the X-Hub-Signature-256 HMAC.

        Meta sends ``sha256=<hex>``; we compute the same HMAC with
        ``app_secret`` as the key and ``raw_body`` (UTF-8 bytes, not
        re-serialized JSON) as the message. Constant-time compare.
        Fzsha256=Nr  )rm   r   r(   r3   r  newr  hashlibsha256	hexdigestr  r4   )r   raw_bodyheaderexpected_hexcomputeds        r.   r  z&WhatsAppCloudAdapter._verify_signature  s     v  +c)no.44688##G,NN
 )+	 	 ""NN##%|'9'9';'B'B'D
 	
r<   c                   |sy|| j                   v r| xj                  dz  c_        yd| j                   |<   t        | j                         t        kD  r9| j                   j	                  d       t        | j                         t        kD  r9y)zReturn True if this wamid is being seen for the first time.

        Returns False (and increments duplicate counter) if the wamid is
        already in the in-memory cache. Cache is FIFO-evicted at
        ``WAMID_DEDUP_CACHE_SIZE``.
        Tr"   Fr   )r   r   r(   WAMID_DEDUP_CACHE_SIZEr   )r   r  s     r.   _dedup_wamidz!WhatsAppCloudAdapter._dedup_wamid  s      D%%%!!Q&!#'% $##$'==%%5%1 $##$'==r<   c           
       K   |j                  d      dk7  r&t        j                  d|j                  d             y|j                  d      xs g D ]Q  }t        |t              s|j                  d      xs g D ]$  }t        |t              s|j                  d      dk7  r*|j                  d	      xs i }|j                  d
      xs g }|j                  d      xs i }i }|D ]  }t        |t              st        |j                  d      xs d      j                         }	|j                  d      xs i }
t        |
j                  d      xs d      j                         }|	s|||	<    |j                  d      xs g D ]  }t        |t              st        |j                  d      xs d      j                         }| j                  |      st        j                  d|       h	 | j                  |||       d{   }|| xj                  dz  c_        	 | j                  |       d{     |j                  d      xs g D ]H  }t        |t              st        j                  d|j                  d      |j                  d             J ' T y7 # t        $ r t        j                  d|       Y Cw xY w7 # t        $ r t        j                  d|       Y kw xY ww)u  Walk a verified Meta webhook payload and dispatch each message.

        Payload shape (truncated):
          {object, entry: [{id, changes: [{value: {messages, contacts,
          statuses, metadata}, field: "messages"}]}]}

        We surface ``messages`` events as MessageEvents; ``statuses``
        events (sent/delivered/read/failed) are logged but not dispatched
        — the agent doesn't currently consume delivery receipts and
        forwarding them would create noisy synthetic events.
        objectwhatsapp_business_accountz6[whatsapp_cloud] ignoring non-WABA payload (object=%r)Nr   changesfieldr   r   contactsr  wa_idrF   profilerl  r  z-[whatsapp_cloud] duplicate wamid %s, skippingz3[whatsapp_cloud] failed to build event for wamid %sr"   z3[whatsapp_cloud] handle_message raised for wamid %sstatusesz![whatsapp_cloud] status %s for %sr  )r5   r   r  r  r  ri   r3   r  _build_message_event_from_cloudr   r   r   handle_message)r   r  r   changer   r  r  contacts_by_waidcontactr  r  rl  raw_messager  eventr  s                   r.   r  z&WhatsAppCloudAdapter._dispatch_payload  s     ;;x $??LLHH% [[)/R H	EeT*))I.4" E!&$/::g&*4 

7+1r 99Z06B 99Z06B 46 ' 7G%gt4 G 4 :;AACE%kk)4:Gw{{628b9??AD26(/7 $)99Z#8#>B $K%k48  5 ;<BBDE,,U3K! !!&*&J&J')98' ! } ((A-("11%888;$P $ii
39r F!&$/?"JJx0"JJt,EH	J! % 
! ((Q! !
! 9$  ((Q!s   E*K8-BK82J*J(
	J*K8&K:K
;K?*K8*>K8(J**K		K8K	K8KK5	1K84K5	5K8c                t
  K   |j                  d      xs i }|j                  d      xs |j                  d      xs i }t        |j                  d      xs d      j                         }|syt        |j                  d      xs d      j                         }| j                  |      st        j                  d|xs d	|       y
|j                  d      r0|j                  dd      }t        |      dk7  ry|\  }}	}
| j                  j                  |	d      }|st        j                  d|	       y	 ddlm} |
dk(  ro	 ddlm}  ||	      }|st        j                  d|	       y|| j                  |	<   	 | j%                  t        |j                  d      xs d      d       d{    y
	 t'        |
      }t        |j                  d      xs t        |dz               } ||	|      }|st        j                  d|	       yy
|j                  d      r|j                  dd      }t        |      dk7  ry|\  }}}
| j*                  j                  |d      }|st        j                  d|       y|
d vr|| j*                  |<   y	 dd!lm}  |||
      }|st        j                  d#|       	 |
d$k(  rd%nd&}| j%                  t        |j                  d      xs d      |       d{    y
|j                  d(      r|j                  dd      }t        |      dk7  ry|\  }}
}| j0                  j                  |d      }|st        j                  d)|       y|
d*vr|| j0                  |<   y	 dd+lm} 	 |j7                  |||
       d{   }|r8	 | j%                  t        |j                  d      xs d      |       d{    y
y
y# t        $ r t        j                  d       Y yw xY w# t         $ r t        j#                  d|	       d}Y w xY w7 u# t         $ r t        j#                  d       Y y
w xY w# t(        $ r( t        j                  d|
       || j                  |	<   Y yw xY w# t        $ r t        j                  d"       Y yw xY w7 # t         $ r t        j#                  d'       Y y
w xY w# t        $ r t        j                  d,       Y yw xY w7 W# t         $ r t        j#                  d-       Y y
w xY w7 E# t         $ r t        j#                  d.       Y y
w xY ww)/u9  Route an inbound interactive reply to the matching resolver.

        Returns True if the tap was claimed (caller should drop the
        webhook entry without dispatching a fresh conversation turn).
        Returns False when the id has no recognized prefix, no live
        state entry, or the resolver itself reports no waiter — in
        those cases the caller falls back to standard text-event
        dispatch, which treats the button title as a normal user
        message. That graceful fallback covers stale-tap and
        cross-process-restart scenarios.

        Dispatch table:
          ``cl:<clarify_id>:<idx|other>``  → resolve_gateway_clarify
          ``appr:<approval_id>:approve|deny`` → resolve_gateway_approval
          ``sc:<once|always|cancel>:<confirm_id>`` → slash_confirm.resolve
        r!  button_reply
list_replyr  rF   FfromzM[whatsapp_cloud] Rejected unauthorized interactive tap from %s (button_id=%r)z	<unknown>Tr8  r9     r*  Nuj   [whatsapp_cloud] clarify tap with no matching state (clarify_id=%s) — likely stale; falling back to textr   )resolve_gateway_clarifyzL[whatsapp_cloud] clarify resolver unavailable; falling back to text dispatchother)mark_awaiting_textz1[whatsapp_cloud] mark_awaiting_text failed for %sz\[whatsapp_cloud] clarify 'Other' tap but entry missing (clarify_id=%s); falling back to textu   ✏️ Type your answer:z,[whatsapp_cloud] clarify other-prompt failedz3[whatsapp_cloud] clarify tap had non-int choice: %rr;  r"   u]   [whatsapp_cloud] clarify resolver reported no waiter (clarify_id=%s) — falling back to textrX  ul   [whatsapp_cloud] approval tap with no matching state (approval_id=%s) — likely stale; falling back to text)approvedeny)resolve_gateway_approvalz.[whatsapp_cloud] approval resolver unavailableub   [whatsapp_cloud] approval resolver reported no waiter (session_key=%s) — likely already resolvedr  u   ✅ Approved.u   ❌ Denied.z([whatsapp_cloud] approval confirm failedzsc:uZ   [whatsapp_cloud] slash_confirm tap with no matching state (confirm_id=%s) — likely stale)oncealwayscancel)slash_confirmz3[whatsapp_cloud] slash_confirm resolver unavailablez-[whatsapp_cloud] slash_confirm.resolve failedz+[whatsapp_cloud] slash_confirm reply failed)r5   ri   r3   r   r   r   r   r2   r(   r   popr   tools.clarify_gatewayr  ImportErrorr  r   r   r  rr   r)   r   tools.approvalr  r   toolsr  resolve)r   r  r
  interinner	button_idr   partsr~  rN  choicerO  r  r  flippedr  response_textresolvedra  r  countconfirm_textrf  _slash_confirm_modresult_texts                            r.   _dispatch_interactive_replyz0WhatsAppCloudAdapter._dispatch_interactive_reply`  st    * .4" 		.)JUYY|-DJ		$-2.446	/526<<>	55i@NN)([	  &OOC+E5zQ$)!Az6--11*dCKM
 I  $H0<G 
 KKH"
 ! 3>##J/U))KOOF39r:2   	&k  		' 2 Bc#'lCM.z=IH ?@J  (OOC+E5zQ%*"A{F3377TJKN
 009D))+6C -[&AECM'-':O  iiKOOF$;$Ar BLQQQ  &OOC+E5zQ$)!Avz3377
DIK78B 998C))*5E$6$>$>V%  T))C(?(E2$FTTT 
 {  4 & ! $$$K" $G$, ! U$$%STU
  I
 3>##J/Z  D 	" R M  !KLM,  I 	    !PQ
 U  T$$%RSTs  D4T87O =T8O8 (T8:1P# +P ,P# 0T82Q =CT8Q; !T84:R" .R/R" 3BT86S =S- S*S- T81T TT T8O52T84O55T88!PT8PT8 P# #QT8QT8.Q85T87Q88T8;RT8RT8R" "S T8ST8S'$T8&S''T8*S- -TT8TT8T T52T84T55T8c                V  K   t        |j                  d      xs d      j                         }|dk(  r| j                  ||       d{   }|ryd}|dk(  r4|j                  d      xs i }t        |j                  d      xs d      }n|dv r|dk(  r2t        |j                  d      xs i j                  d      xs d      }n|j                  d      xs i }|j                  d	      xs |j                  d
      xs i }	t        |	j                  d      xs d      }n7|dv r3|j                  |      xs i }	t        |	j                  d      xs d      }t        j
                  t        j                  t        j                  t        j                  t        j                  t        j                  t        j                  t        j
                  t        j
                  t        j
                  t        j
                  dj                  |t        j
                        }
t        |j                  d      xs d      j                         }|j                  |d      }|j                  d      }|r't        j                  d||j                  d             y|}||d|d}| j                  |      syg }g }|dv r2|j                  |      xs i }	t        |	j                  d      xs d      j                         }t        |	j                  d      xs d      j                         }|rd}|rt        |      }| j                  ||       d{   \  }}|rB|j!                  |       |j!                  |xs |xs d       t        j#                  d||       nt        j                  d||       |dk(  r6t        |	j                  d      xs d      j                         }|r|sd| d}d}|dk(  r|r|D ]  }t%        |      j&                  j                         }|dv s+	 t%        |      j)                         j*                  }||kD  rt        j#                  d |||       mt%        |      j-                  d!d"#      }t%        |      j.                  }d$| d%| }|r| d&| n|} |j                  d(      xs i }t        |j                  d      xs d      j                         xs d} d}!d}"| rwt5        j6                  ||       }!t        |j                  d      xs d      j                         }#t        |j                  d)      xs d      j                         }$|#r|$r|#|$k(  }"| j9                  ||xs |d*||xs d+      }%t        |j                  d      xs d      xs d}&|&r8|r6| j;                  | j<                  ||&       |rt5        j>                  ||&|       tA        ||
|%||&| |!|"||,
      S 7 7 # t0        $ r t        j3                  d'|       Y ,w xY ww)-u  Convert a Cloud-API message object into a Hermes MessageEvent.

        Phase 4 expands beyond text to download inbound media (image,
        video, audio/voice, document, sticker) by ``media_id`` via the
        two-step Graph endpoint. Cached files are populated into
        ``media_urls`` / ``media_types`` so the agent's vision and STT
        layers see them. Text-readable documents (.txt, .md, .json,
        source code, etc.) are read and prepended to the message body
        up to 100KB — same heuristic the Baileys adapter uses.

        Returns None if the message is filtered out by the mixin's
        gating (broadcast filter, allow-list, mention requirements).
        r   r   r!  NrF   r,   >   r=  r!  r=  r  r  r;  >   r   r   r   voicer   r   r  )r   r   r   r   r1  r   r   r=  r!  locationr  r  chatu   [whatsapp_cloud] received group-shaped message (chat=%s, wamid=%s) — group support is not yet implemented; dropping. Use the Baileys whatsapp adapter for group chats.r  F)chatIdsenderIdisGroupr,   r{  r  r   z,[whatsapp_cloud] cached inbound %s media: %sum   [whatsapp_cloud] failed to download inbound %s (id=%s) — agent will see message metadata but not the binaryr   r  z[Document: ]r   >   .js.md.py.ts.css.csv.log.txt.xml.yml.html.json.yamlz?[whatsapp_cloud] skipping text injection for %s (%d bytes > %d)r  r   )encodingr  z[Content of z]:
r6  z1[whatsapp_cloud] failed to read document text: %sr   display_phone_numberrk  )r
  	chat_name	chat_typeuser_id	user_name)
r   message_typer  r  r   r.  reply_to_textreply_to_is_own_message
media_urlsmedia_types)!ri   r5   r4   r/  r   TEXTPHOTOVIDEOVOICEDOCUMENTr3   r   r   _should_process_messager;   r  rK  r   r   suffixstatst_size	read_textrl  r  r   r   lookupbuild_sourcer   r   r	  r   )'r   r  r
  r  msg_type_strhandledr,   r   r#  r$  rK  r   sender_name
chat_fieldr
  gating_datarN  rO  r  inbound_mimer  
local_pathdl_mimefnameMAX_TEXT_INJECT_BYTESdoc_pathr  	file_sizer#   display_name	injectionr   reply_to_idrL  reply_to_is_ownquoted_from
our_numberr  r  s'                                          r.   r  z4WhatsAppCloudAdapter._build_message_event_from_cloud8  s    & ;??62<f=CCE =( <<- G 6!??6*0bDtxx'-2.D66 x'KOOH5;@@HNBO#6<"		.1RUYY|5LRPR599W-34XX  OOL17REuyy+1r2D  $$ && && && &&#,,"((!&&&++#((#((
 #lK,,
- 	 /526<<>	&**9b9 !__V,
NND KOOD1	 
 !	
 ++K8 !#
!#VVOOL17RE599T?0b1779Huyy5;<BBDL,\:H,0,I,Ix -J - '#
G %%j1&&w'\,'\B\]KKF$j
 NNM$h  :-		* 5 ;<BBDET!,UG15 !+:%*& 8n++113  $(N$7$7$9$A$A	$'<<"KK!2 ()5J
 %"&x.":":%,Y #; # (,H~':':&2<.WI$N	;?)D7Y)D //),2'++d+1r288:Bd'++227KHM gkk&17R8>>@KX\\*@AGRHNNPJz"-";""!,W!)T # 
 KOOD)/R08DW d>>O &&wt<%# +'$3!#
 	
ul'b # ((O$sY   AX)	W=
LX)X CX)%A X%X)&AX*EX) X)X&"X)%X&&X))r   r   )r   r   returnri   )r   ri   rn  ri   )r   z'OrderedDict[str, str]'r   ri   r   ri   rn  None)rn  ri   )r   set[str]rn  rp  )r   ri   rn  r  rn  r  )r   r  rn  r  )rn  ro  r  )
r
  ri   r#   ri   r  Optional[str]r  Optional[Dict[str, Any]]rn  r   )N)r
  ri   rn  ro  )r
  ri   r"  Dict[str, Any]r  rr  rn  r   )   )r   ri   r'  rr   rn  ri   )i   )r
  ri   rL  ri   rM  zOptional[list]rN  ri   rO  ri   r  rs  rn  r   )zdangerous commandNTF)r
  ri   r\  ri   rO  ri   rC  ri   r  rs  r]  r  r^  r  rn  r   )r
  ri   r;  ri   re  ri   rO  ri   rf  ri   r  rs  rn  r   )r,   rt  r  rr   rn  ri   )r
  ri   rn  rt  )ry  ri   rz  ri   r{  rr  rn  #tuple[Optional[str], Optional[str]])r
  ri   rz  ri   r  rr  r  rr  r  rr  r  rr  r  rr  rn  r   )r
  ri   r  ri   rz  ri   r  rr  r  rr  r  rr  r{  rr  rn  r   )
r
  ri   r  ri   r  rr  r  rr  rn  r   )
r
  ri   r  ri   r  rr  r  rr  rn  r   )
r
  ri   r  ri   r  rr  r  rr  rn  r   )
r
  ri   r  ri   r  rr  r  rr  rn  r   )NNN)r
  ri   ry  ri   r  rr  r  rr  r  rr  rn  r   )r  ri   rn  rr  )r  ri   r  rr  rn  rv  )r*   z'web.Request'rn  z'web.Response')r  bytesr  ri   rn  r  )r  ri   rn  r  )r  rt  rn  ro  )r  rt  r
  r   rn  r  )r  rt  r
  r   r  rt  rn  zOptional[MessageEvent])/__name__
__module____qualname____doc__splits_long_messagesrf   staticmethodru   r   r   r   r}   r   r   r   r   r   r  r  r#  r(  r,  rU  rb  rg  r  rm  r  r  r  r  r  r  r  r  r  r  r  r   r   r   r  r  r  r/  r  __classcell__)r   s   @r.   rC   rC      s<   	  w@t 9 9V & &   1	+. 5: <|"( #'-1LDLD LD  	LD
 +LD 
LD|4V #'	9D9D )9D  	9D
 
9Dv 	1 	1 ) ) .2VV V  	V
 V V +V 
Vz /-1 $"77 7 	7
 7 +7 7 7 
7@ .2.. . 	.
 . . +. 
.` 0 0/ $(	== = !	=
 
-=H #'$(!%"&"&L:L: L:
  L: "L: L:  L:  L: 
L:h "&"&"&#'%
%
 %
 	%
 %
  %
  %
 !%
 
%
V "&"&

 
 	

  
 

, "&"&

 
 	

  
 

" "&"&

 
 	

  
 

" "&"&1
1
 1
 	1

  1
 
1
n "&#'"&

 
 	

 !
  
 

$$L

" #'	\+\+  	\+
 
-\+@
 G>A(H
4(ZxV#V )V 
	Vp`
#`
 )`
 !	`

 
 `
r<   rC   )r*   r   r+   rr   rn  rw  )r8   ri   rn  rr  rq  )?r{  
__future__r   r%   r  r  loggingr6   r{   r   shutilrY  collectionsr   pathlibr   typingr   r   r   aiohttpr	   r>   r  r   r?   gateway.configr
   r   gateway.platforms.baser   r   r   r   r   !gateway.platforms.whatsapp_commonr   gatewayr   hermes_constantsr   	getLoggerrx  r   ry   rp   rs   rv   r   r   r  r   ru  rx  whichr  r    __annotations__r/   r;   r  rA   rC   r@   r<   r.   <module>r     s  'R #      	 	   #  & &
O
 4  D # +			8	$     * -( 
    $  !  * v||H%  6 "N 6& N+KMcde 1Y
02E Y
E  
C  OEs$   C6 D 6	DD	DD