
    `gjW                        d Z ddlmZ ddlZddlZddlZddlmZ ddZddZ	ddZ
ddZdd	ZdddZddd
d	 	 	 	 	 	 	 	 	 ddZddZy)u$  
Interactive setup wizard for the WhatsApp Cloud API adapter.

Entry point: ``hermes whatsapp-cloud`` (dispatched from
``cmd_whatsapp_cloud`` in ``hermes_cli/main.py``).

Walks the user through the 6 credentials Meta requires + recipient
allowlist, auto-generates the verify token, and prints exact follow-up
instructions for the parts that can't happen inside the wizard process
(starting cloudflared, starting the gateway, configuring Meta's
webhook dashboard, adding their phone to the recipient list).

Heavy emphasis on field-shape validation to catch the most common
configuration mistakes:

- Putting the actual phone number in ``WHATSAPP_CLOUD_PHONE_NUMBER_ID``
  (the field expects Meta's 15-17 digit internal ID, not a phone number).
  This is the #1 trap — caught us during Phase 3 live testing.
- Pasting tokens with trailing whitespace.
- Pasting an OpenAI / Slack / GitHub key by mistake.
- Confusing App ID with WABA ID with Phone Number ID.

Each prompt has contextual help showing exactly where to find the value
in Meta's App Dashboard, with a one-line description and the field's
expected shape ("starts with EAA", "15-17 digits", "32 hex chars", etc.).

The wizard intentionally does NOT smoke-test the webhook itself — the
Hermes gateway and the cloudflared tunnel both run in separate
processes the user starts AFTER this wizard exits, so any in-wizard
probe would fail by design. Instead the final SETUP COMPLETE block
prints the exact curl command the user can run from a third terminal
to verify the loop end-to-end once everything's running.
    )annotationsN)Optionalc                    | sy| j                         }|j                         sydt        |      cxk  rdk  ry t        |      dk  ryt        |      dkD  ry	y
)u'  Phone Number ID is a 15-17 digit numeric ID assigned by Meta.

    It's NOT a phone number. The #1 setup mistake is pasting the actual
    phone number (e.g. ``15556422442``) into this field — that's only
    10-11 digits and gets rejected by Graph as "Object with ID does
    not exist."
    )FzPhone Number ID is required)Fz;Phone Number ID must be numeric (no '+', spaces, or dashes)
      )Fu   That looks like a phone number — but this field needs the Phone Number ID (Meta's internal ID, 15-17 digits, e.g. '7794189252778687'). Look just BELOW the 'From' dropdown in API Setup → it's labelled 'Phone number ID'.   )Fz7Phone Number ID looks too short (expected 13-18 digits)   )Fz6Phone Number ID looks too long (expected 13-18 digits)TNstripisdigitlenvaluess     R/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/setup_whatsapp_cloud.py_validate_phone_number_idr   4   s_     3A99;S
 
SVr
  1v{O
1v{N    c                    | sy| j                         }|j                         syt        |      dk  st        |      dkD  ryy)z<WABA ID is numeric, similar length range as Phone Number ID.)FzWABA ID is required)FzWABA ID must be numericr      )Fz+WABA ID looks wrong (expected 10-25 digits)r
   r   r   s     r   _validate_waba_idr   S   s:    +A99;/
1v{c!frkCr   c                    | sy| j                         }|j                         syt        |      dk  st        |      dkD  ryy)z/Meta App ID is numeric, typically 15-16 digits.)FzApp ID is required)FzApp ID must be numericr   r	   )Fz*App ID looks wrong (expected 15-16 digits)r
   r   r   s     r   _validate_app_idr   _   s:    *A99;.
1v{c!frkBr   c                    | sy| j                         }t        j                  d|j                               syt	        |      dk7  rddt	        |       dfS y)	z2App Secret is a 32-character lowercase hex string.)FzApp Secret is requiredz	[0-9a-f]+)Fu   App Secret should be a hex string (only digits 0-9 and letters a-f). Make sure you copied the 'App secret' from Settings → Basic, not some other token.    Fz4App Secret should be exactly 32 hex characters (got )r
   )r   re	fullmatchlowerr   r   s     r   _validate_app_secretr    k   sW    .A<<aggi0

 1v|LSQRVHTUVVVr   c                <   | sy| j                         }|j                  d      sY|j                  d      ry|j                  d      s|j                  d      ry|j                  d      s|j                  d	      ry
yt        |      dk  rddt        |       dfS y)zMeta access tokens start with ``EAA`` and are 100-300+ characters.

    Both temp tokens (24h) and System User permanent tokens share this
    prefix. We don't try to distinguish them.
    )FzAccess token is requiredEAAzsk-)FziThat's an OpenAI key (starts with 'sk-'), not a Meta WhatsApp access token. Meta tokens start with 'EAA'.zxoxb-zxoxp-)FzUThat's a Slack token, not a Meta WhatsApp access token. Meta tokens start with 'EAA'.ghp_gho_)FzVThat's a GitHub token, not a Meta WhatsApp access token. Meta tokens start with 'EAA'.)Fu   Meta WhatsApp access tokens start with 'EAA'. Check that you're copying from the right place (API Setup → 'Generate access token', or Business Settings → System Users → 'Generate token' for a permanent one).d   FzAccess token looks too short (z chars, expected 100+)r
   )r   
startswithr   r   s     r   _validate_access_tokenr'   {   s     0A<<<< << ALL$9 <<1<<#7
 1v|6s1vh>TUUUr   Fc                ,   	 |rd| dnd}|rHt         j                  j                         r*ddl}|j                  |  | d      j	                         }|S t        |  | d      j	                         }	 |S # t        t        f$ r t                Y yw xY w)u  Read one line of input. Returns "" on EOF / Ctrl+C / empty input.

    The ``default`` parameter is shown to the user but NOT auto-applied
    on empty input — callers handle the "user kept existing" case
    explicitly so they can distinguish between a real value and a
    display preview (e.g. ``"abc12345..."`` for masked secrets).

    ``secret=True`` reads via ``getpass`` so credentials are not echoed
    to the terminal (or left in scrollback).
     [] r   Nz (input hidden): : )	sysstdinisattygetpassr   inputEOFErrorKeyboardInterruptprint)messagedefaultsecretsuffixr0   raws         r   _promptr:      s    
$+2gYacii&&(//WIfX5F"GHNNPC J	 7)F82./557C J '( s   AA7 A7 7BB)current	help_textr7   c               j   |r1|j                         j                         D ]  }t        d|         d}	 |dz  }t        d|  ||      }|sy ||      \  }}	|r|j                         S t        d|	        |dk\  r	 t	        d	      j                         }
|
syd}i# t
        t        f$ r Y yw xY w)
a	  Repeat the prompt until the user enters a valid value or aborts.

    Returns the validated value, or None if the user gave up (empty
    response after an error, or Ctrl+C). ``current`` is shown as a
    default for re-runs of the wizard with existing config.
    z  r      u     → )r6   r7   Nu       ✗    z'    Try again, or press Enter to skip: )r   
splitlinesr4   r:   r1   r2   r3   )r5   	validatorr;   r<   r7   lineattemptsr   okreasonconts              r   _prompt_validatedrG      s     OO%002 	DBtf+	H
A&	*GFKu%
F;;= !"q=FGMMO H!  /0 s   B   B21B2c                    ddl m} m} t                t        d       t        d       t                t        d       t        d       t                t        d       t        d       t        d	       t        d
       t        d       t                t        d       t        d       t                t        d       t        d       t        d       t        d       t        d       t                	 t	        d      j                         }t                t        d       t        d       t        d        | d      xs d}t        dt        |d      }|s|r|}t        d|        n#t        d       y |d|       t        d|        t                t        d       t        d        t        d        | d!      xs d}|r|dd" d#z   nd}t        d$t        |d%d&'      }|s|r|}t        d(       n t        d)       y |d!|       t        d*       t                t        d       t        d+       t        d        | d,      xs d}|r|dd- d#z   nd}	t        d.t        |	d%d/'      }
|
s'|r|}
t        d0       n+t        d1       t        d2       n |d,|
       t        d3       t                t        d       t        d4       t        d        | d5      xs d}t        d6d7 |d8      }|r |d5|       t        d|        n|rt        d|         | d9      xs d}t        d:d; |d<      }|r |d9|       t        d|        n|rt        d|        t                t        d       t        d=       t        d        | d>      xs d}|ryt        d?|dd-  d@       	 t	        dA      j                         j                         }|dCv r-t        j                  dD      } |d>|       t        dE|        n:|}t        dF       n,t        j                  dD      } |d>|       t        dG|        t                t        dH       t        dI       t                t        d       t        dJ       t        d       t                t        dK       t        dL       t        dM       t        dN       t                 | dO      xs d}|r|nd}	 t	        dP|rdQ|z   dRz   ndS dT      j                         xs |xs dS}|r?dUj                  dV |j!                  dU      D              } |dO|       t        d|        nt        dW       t        dX       t                t        d       t        dY       t        d       t                t        dZ       t        d[       t        d\       t                t        d]       t        d^       t        d_       t        d`       t                t        da       t        db       t                t        dc       t        dd       t        de       t                t        df       t        dg       t                t        dh       t        di       t                t        dj       t        dk| dl       t        dm       t                t        dn       t        do       t        dp       t                t        dq       t        dr       t        ds       t        dt|        t        du       t        dv       t                t        dw       t        dx       t        dy       t                t        dz       t                t        d       t        d{       t        d       t                t        d|       t        d}       t        d~       t        d       t                |xs |}|rt        d       t        d|        n!t        d       t        d       t        d       t        d       t                t        d       t        d       t                t        d       t        d       t        d       t                t        d       t        d       t                y# t        t        f$ r t        d       Y yw xY w# t        t        f$ r dB}Y w xY w# t        t        f$ r dS}Y w xY w)zInteractive wizard for the WhatsApp Cloud API adapter.

    Returns 0 on full success, 1 on user abort, 2 on partial completion
    (some fields written but the user bailed before finishing).
    r   )get_env_valuesave_env_valueu%   ⚕ WhatsApp Business Cloud API Setupz2==================================================z<This wizard configures Hermes to talk to WhatsApp via Meta'sz3official Cloud API. It's the production-grade path:u/     • No QR codes, no Node.js bridge subprocessu/     • Stable connection — no account-ban risku7     • Business account required (not personal WhatsApp)u<     • Public webhook URL required (Cloudflare Tunnel, ngrok,z'    or your own reverse proxy with TLS)z;If you don't have a Meta app set up yet, follow these stepsz-FIRST, then come back and re-run this wizard:u8     1. https://developers.facebook.com/apps → Create Appu2        → 'Connect with customers through WhatsApp'u-     2. App Dashboard → WhatsApp → API Setupz=  3. Click 'Generate access token' (temp 24h token is fine toz:     start; switch to a System User permanent token later)z/Press Enter to continue, or Ctrl+C to abort... z
Setup cancelled.r>   u   ──────────────────────────────────────────────────u   STEP 1 — Phone Number IDWHATSAPP_CLOUD_PHONE_NUMBER_IDNzPhone Number IDu8  Found in: App Dashboard → WhatsApp → API Setup, in the
'Send and receive messages' section.
Look BELOW the 'From' dropdown — there's a 'Phone number ID'
line with the value (15-17 digits, e.g. '7794189252778687').
It is NOT the phone number itself (+1 555-...). That's the
single most common setup mistake.)r;   r<   u     ✓ Keeping existing: u+   
✗ Phone Number ID is required. Aborting.u     ✓ Saved: u   STEP 2 — Access TokenWHATSAPP_CLOUD_ACCESS_TOKEN   z...zAccess TokenTu  Two options for getting one:

  (a) TEMP — App Dashboard → WhatsApp → API Setup →
      'Generate access token' button. Lasts 24 hours.
      Fine for testing today; you'll have to regenerate
      tomorrow.

  (b) PERMANENT (production) — System User token. One-time
      setup, never expires:
      • business.facebook.com → Settings → System users →
        Add → Admin role
      • Assign Assets → your app (Manage app), your
        WhatsApp account (Manage WABAs)
      • Generate token → expiration: Never → permissions:
        business_management, whatsapp_business_messaging,
        whatsapp_business_management

Tokens start with 'EAA'.)r;   r7   r<   u     ✓ Keeping existing tokenu(   
✗ Access Token is required. Aborting.u     ✓ Saved (token hidden)uC   STEP 3 — App Secret (required for webhook signature verification)WHATSAPP_CLOUD_APP_SECRET   z
App SecretuU  Found in: App Dashboard → Settings → Basic →
'App secret' field (click 'Show', enter your Facebook password).

If 'Show' doesn't appear, you may need Admin role on the app.
It's a 32-character lowercase hex string.

Without the App Secret, inbound webhook POSTs are refused
with HTTP 503 (we can't verify they actually came from Meta).u!     ✓ Keeping existing App Secretu=   
⚠ Skipping App Secret — inbound webhooks will be refusedz4   until you set WHATSAPP_CLOUD_APP_SECRET manually.u     ✓ Saved (secret hidden)u5   STEP 4 — App ID & WABA ID (optional, for analytics)WHATSAPP_CLOUD_APP_IDz&App ID (optional, press Enter to skip)c                     | sdS t        |       S Nr
   )r   vs    r   <lambda>z*run_whatsapp_cloud_setup.<locals>.<lambda>x  s    a, -=a-@ r   u   Found in: App Dashboard → Settings → Basic → 'App ID' at the
top of the page. Numeric, ~15-16 digits.
Not required for messaging — useful only for analytics later.WHATSAPP_CLOUD_WABA_IDz'WABA ID (optional, press Enter to skip)c                     | sdS t        |       S rR   )r   rS   s    r   rU   z*run_whatsapp_cloud_setup.<locals>.<lambda>  s    a, ->q-A r   u   WhatsApp Business Account ID. Found in: App Dashboard →
WhatsApp → API Setup, near the top — 'WhatsApp Business
Account ID'. Numeric, ~15+ digits.
Not required for messaging — useful for analytics.u(   STEP 5 — Verify Token (auto-generated)WHATSAPP_CLOUD_VERIFY_TOKENz+  An existing verify token is already set (z...).z  Generate a new one? [y/N]: n>   yyesr   u     ✓ New verify token: u#     ✓ Keeping existing verify tokenu     ✓ Generated: u>     → COPY THIS TOKEN NOW. You'll paste it into Meta's webhookz%    configuration dialog (next step).u   STEP 6 — Recipient Allowlistz;  Who is allowed to message the bot? (Comma-separated phonez>  numbers with country code, no '+' / spaces / dashes. Use '*'u@     to allow anyone — only safe if you've also configured Meta'sz0  recipient whitelist for app-development mode.)WHATSAPP_CLOUD_ALLOWED_USERSu     → Allowed usersr)   r*   r+   r,   ,c              3  l   K   | ],  }|j                         st        j                  d d|       . yw)z[\s\-+]r+   N)r   r   sub).0parts     r   	<genexpr>z+run_whatsapp_cloud_setup.<locals>.<genexpr>  s*      
-14::<BFF:r4(
s   44u<     ⚠ No allowlist — every inbound message will be denied.zD    Re-run this wizard or set WHATSAPP_CLOUD_ALLOWED_USERS manually.u   SETUP COMPLETE — Next stepsz?  Hermes needs a public HTTPS URL to receive WhatsApp messages.z:  The recommended path is Cloudflare Tunnel (free, no portz  forwarding, no DNS setup).z<    1. Install cloudflared (one-time, if you don't have it):z8         Windows:  winget install Cloudflare.cloudflaredz+         macOS:    brew install cloudflaredzE         Linux:    https://github.com/cloudflare/cloudflared/releasesz>       Alternatives: ngrok, or your own domain + reverse proxyz       with TLS.z/    2. Start the tunnel in a separate terminal:z7         cloudflared tunnel --url http://localhost:8090z?       Note the printed https://<random>.trycloudflare.com URL.z4    3. Start the Hermes gateway in another terminal:z         hermes gatewayz:    4. Verify your local config is reachable. From a thirdz1       terminal, with the tunnel URL substituted:zG         curl 'https://YOUR-TUNNEL.trycloudflare.com/whatsapp/webhook?\z3               hub.mode=subscribe&hub.verify_token=z&\z#               hub.challenge=hello'z,       Expected: HTTP 200 with body 'hello'.zB       Also try: curl https://YOUR-TUNNEL.trycloudflare.com/healthz?       (should return JSON with verify_token_configured: true).z.    5. Configure Meta to point at your tunnel:uF            App Dashboard → WhatsApp → Configuration → Edit webhookz4         Callback URL: <tunnel-url>/whatsapp/webhookz         Verify Token: u$            → Click 'Verify and save'uE            → Then 'Manage' webhook fields → subscribe to 'messages'z/    6. Add your phone to Meta's recipient list:u>            App Dashboard → WhatsApp → API Setup → 'To' →z#         'Manage phone number list'z0    7. DM the bot's test number from your phone.z,Optional: polish your bot's WhatsApp profilez@  WhatsApp shows a display name and profile picture for your botz@  in every chat header and contact list. These are set in Meta'suB     Business Manager, not via this wizard — but here's where to doz   it once you're up and running:u'       • Display name + profile picture:zG        https://business.facebook.com/wa/manage/phone-numbers/?waba_id=z>        https://business.facebook.com/wa/manage/phone-numbers/z<        (select your WhatsApp Business Account on that page)z>        Display-name changes go through a ~24-48h Meta review.u>       • About, description, website, hours, business category:uA           Same page → click your phone number → 'Edit profile'.u)       • Verified badge (the green check):u9           Requires Meta's business verification process —uD           Business Manager → Security Center → Start Verification.z>  Docs: https://hermes-agent.nousresearch.com/docs/user-guide/z         messaging/whatsapp-cloud)hermes_cli.configrI   rJ   r4   r1   r   r2   r3   rG   r   r'   r    r   secretstoken_urlsafejoinsplit)rI   rJ   proceedcurrent_phone_idphone_idcurrent_tokencurrent_displaytokencurrent_secretcurrent_secret_display
app_secretcurrent_app_idapp_idcurrent_waba_idwaba_idcurrent_verifyregenverify_tokencurrent_allowallow_defaultallowedeffective_wabas                         r   run_whatsapp_cloud_setupr|      s(    @	G	
12	(O	G	
HI	
?@	G	
;<	
;<	
CD	
HI	
34	G	
GH	
9:	G	
DE	
>?	
9:	
IJ	
FG	GIJPPR
 
G	*	
&'	*$%EFN$ ! 0H 'H,XJ78@A7BhZ()	G	*	
#$	*!"?@HDM6C}Sb)E1O'E0 !E01=>4e<*+	G	*	
OP	*"#>?G4N=KnRa058QU"&LJ 'J56RSHI2J?+,	G	*	
AB	*"#:;CtN0@N	F .7fX&'	((89:#$<=EO1AC
G /9gY'(	((9:;	G	*	
45	*"#@AITN;N2A<N;OuUV	9:@@BHHJE L "004L8,G,\N;<)L78,,R04lC!,01	G	
JK	
12	G	*	
*+	*	G	
GH	
JK	
LM	
<=	G!"@AITM%2MM!$"6"<SU!VVXY

%'+#)r 	
 (( 
5<]]35G
 
 	5w?gY'(LMTU	G	*	
)*	*	G	
KL	
FG	
()	G	
HI	
DE	
78	
QR	G	
JK	
	G	
;<	
CD	
KL	G	
@A	
#$	G	
FG	
=>	G	
TU	?~S
QR	
/0	G	
89	
NO	
KL	G	
:;	
RS	
@A	#L>
23	
01	
QR	G	
;<	
JK	
/0	G	
<=	G	*	
89	*	G	
LM	
LM	
NO	
,-	G/N78 ()+ 	, 	78NOLM	
JK	G	
JK	
MN	G	
56	
EF	
PQ	G	
JK	
,-	Gk '( "#r +, 	E	B '( s6   6b 1'b6 !/c b32b36c
cc#"c#)r   strreturnztuple[bool, Optional[str]])NF)r5   r}   r6   Optional[str]r7   boolr~   r}   )
r5   r}   r;   r   r<   r   r7   r   r~   r   )r~   int)__doc__
__future__r   r   rd   r-   typingr   r   r   r   r    r'   r:   rG   r|    r   r   <module>r      s    D # 	  
 >		 "T: "#"" 	"
 " " "Tur   