
    epjU                     D   d 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
mZmZ ddlmZmZ ddlmZmZmZmZ ddlmZmZ ddlmZ ddlmZ dd	Z ej        e          Z d
Z!dZ"dZ#dZ$dZ%de&fdZ' G d de          Z(de)de)fdZ*dddddZ+de&fdZ,d Z-ddZ.dS )u  SMS (Twilio) platform adapter.

Connects to the Twilio REST API for outbound SMS and runs an aiohttp
webhook server to receive inbound messages.

Shares credentials with the optional telephony skill — same env vars:
  - TWILIO_ACCOUNT_SID
  - TWILIO_AUTH_TOKEN
  - TWILIO_PHONE_NUMBER  (E.164 from-number, e.g. +15551234567)

Gateway-specific env vars:
  - SMS_WEBHOOK_PORT     (default 8080)
  - SMS_WEBHOOK_HOST     (default 127.0.0.1)
  - SMS_WEBHOOK_URL      (public URL for Twilio signature validation — required)
  - SMS_INSECURE_NO_SIGNATURE  (true to disable signature validation — dev only)
  - SMS_ALLOWED_USERS    (comma-separated E.164 phone numbers)
  - SMS_ALLOW_ALL_USERS  (true/false)
  - SMS_HOME_CHANNEL     (phone number for cron delivery)
    N)AnyDictOptional)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResult)redact_phonestrip_markdown)UnscopedSecretError)
get_secretc                 z    	 t          | |          }n$# t          $ r t          j        |           }Y nw xY w||n|S )a  Scope-aware credential read with the default-profile startup fallback.

    Secondary profiles construct their adapters under a profile secret
    scope -- the scope is authoritative and a scoped miss returns ``default``
    (no cross-profile borrow from ``os.environ``, which may hold another
    profile's value). The DEFAULT profile's adapter constructs and sends
    *unscoped* under multiplexing, where a bare ``get_secret`` would raise
    ``UnscopedSecretError`` and crash this path; there ``os.environ`` is that
    profile's own value, so fall back to it. Same pattern as the Slack
    ``SLACK_APP_TOKEN`` read (#59739) and
    ``gateway/platforms/whatsapp_common.py::_get_wsecret``.
    )_scoped_get_secret_UnscopedSecretErrorosgetenv)namedefaultvals      C/home/thesage/.hermes/hermes-agent/plugins/platforms/sms/adapter.py_get_scoped_secretr   +   sR     w//   ioo/33w.s    44z*https://api.twilio.com/2010-04-01/Accountsi@  i  z	127.0.0.1i   returnc                      	 ddl } n# t          $ r Y dS w xY wt          t          d          ot          d                    S )z0Check if SMS adapter dependencies are available.r   NFTWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN)aiohttpImportErrorboolr   )r   s    r   check_sms_requirementsr!   H   s[       uu"#788d=OPc=d=deees    
c                   .    e Zd ZdZeZdef fdZdefdZ	ddde
de
fd	ZddZ	 	 ddededee         deeeef                  def
dZdedeeef         fdZdedefdZdededede
fdZdededede
fdZededed
z  fd            ZddZ xZS )
SmsAdapterz
    Twilio SMS <-> Hermes gateway adapter.

    Each inbound phone number gets its own Hermes session (multi-tenant).
    Replies are always sent from the configured TWILIO_PHONE_NUMBER.
    configc                    t                                          |t          j                   t	          dd          | _        t	          dd          | _        t          j        dd          | _	        t          t          j        dt          t                                        | _        t          j        dt                    | _        t          j        dd                                          | _        d | _        d | _        d S )Nr    r   TWILIO_PHONE_NUMBERSMS_WEBHOOK_PORTSMS_WEBHOOK_HOSTSMS_WEBHOOK_URL)super__init__r   SMSr   _account_sid_auth_tokenr   r   _from_numberintstrDEFAULT_WEBHOOK_PORT_webhook_portDEFAULT_WEBHOOK_HOST_webhook_hoststrip_webhook_url_runner_http_session)selfr$   	__class__s     r   r,   zSmsAdapter.__init__[   s    ...!34H"!M!M 23F K K!#+@"!E!E"%I(#.B*C*CDD#
 #
 #%),>@T"U"U!#+<b!A!A!G!G!I!I@D    r   c                     | j          d| j         }t          j        |                    d                                        d          }d| S )z.Build HTTP Basic auth header value for Twilio.:asciiBasic )r.   r/   base64	b64encodeencodedecode)r;   credsencodeds      r   _basic_auth_headerzSmsAdapter._basic_auth_headerh   sS    $99t'799"5<<#8#899@@II!!!!r=   F)is_reconnectrI   c                D  K   dd l }ddl m | j        s6d}t                              |           |                     d|d           dS t          j        dd                                          d	k    }| j	        s8|s6d
}t                              |           |                     d|d           dS |r'| j	        s t          
                    d| j                                       t                    }|j                            d| j                   |j                            dfd                               |          | _        | j                                         d {V                      | j        | j        | j                  }|                                 d {V  |                    |                    d          d          | _        d| _        t                              d| j        | j        t;          | j                             dS )Nr   webu9   [sms] TWILIO_PHONE_NUMBER not set — cannot send repliessms_missing_phone_numberF)	retryableSMS_INSECURE_NO_SIGNATUREr&   truea+  [sms] Refusing to start: SMS_WEBHOOK_URL is required for Twilio signature validation. Set it to the public URL configured in your Twilio console (e.g. https://example.com/webhooks/twilio). For local development without validation, set SMS_INSECURE_NO_SIGNATURE=true (NOT recommended for production).sms_missing_webhook_urlu   [sms] SMS_INSECURE_NO_SIGNATURE=true — Twilio signature validation is DISABLED. Any client that can reach port %d can inject messages. Do NOT use this in production.)client_max_sizez/webhooks/twilioz/healthc                 0                         d          S )Nok)text)Response)_rL   s    r   <lambda>z$SmsAdapter.connect.<locals>.<lambda>   s    $0G0G r=      totalTtimeout	trust_envz8[sms] Twilio webhook server listening on %s:%d, from: %s)r   rL   r0   loggererror_set_fatal_errorr   r   lowerr8   warningr4   Application_TWILIO_WEBHOOK_MAX_BODY_BYTESrouteradd_post_handle_webhookadd_get	AppRunnerr9   setupTCPSiter6   startClientSessionClientTimeoutr:   _runninginfor   )r;   rI   r   msginsecure_no_sigappsiterL   s          @r   connectzSmsAdapter.connectr   sQ       	MCLL!!"<cU!SSS5)$?DDJJLLPVV  
	 
	S  LL!!";SE!RRR5 	4#4 	NN1 "	   oo.LoMM
.0DEEE
9&G&G&G&GHHH}}S))l  """""""""{{4<);T=OPPjjll$22)))33 3 
 
 F*++		
 	
 	
 tr=   Nc                    K   | j         r&| j                                          d {V  d | _         | j        r&| j                                         d {V  d | _        d| _        t
                              d           d S )NFz[sms] Disconnected)r:   closer9   cleanuprp   r_   rq   )r;   s    r   
disconnectzSmsAdapter.disconnect   s       	&$**,,,,,,,,,!%D< 	 ,&&(((((((((DL()))))r=   chat_idcontentreply_tometadatac           	        K   dd l }|                     |          }|                     |          }t          d          }t           d| j         d}	d|                                 i}
| j        p*|                    |	                    d          d	          }	 |D ]}|
                                }|                    d
| j                   |                    d|           |                    d|           	 |                    |	||
          4 d {V }|                                 d {V }|j        dk    r|                    dt#          |                    }t$                              dt)          |          |j        |           t          dd|j         d|           cd d d           d {V  c | j        s|r|                                 d {V  S S S |                    dd          }t          d|          }d d d           d {V  n# 1 d {V swxY w Y   # t,          $ rx}t$                              dt)          |          |           t          dt#          |                    cY d }~c | j        s|r|                                 d {V  S S S d }~ww xY w	 | j        s|r|                                 d {V  n*# | j        s|r|                                 d {V  w w w xY w|S )Nr   T)success//Messages.jsonAuthorizationrY   rZ   r\   FromToBodydataheaders  messagez[sms] send failed to %s: %s %sFzTwilio z: )r   r`   sidr&   )r   
message_idz[sms] send error to %s: %s)r   format_messagetruncate_messager   TWILIO_API_BASEr.   rH   r:   rn   ro   FormData	add_fieldr0   postjsonstatusgetr2   r_   r`   r   rx   	Exception)r;   r{   r|   r}   r~   r   	formattedchunkslast_resulturlr   sessionchunk	form_datarespbody	error_msgmsg_sides                      r   sendzSmsAdapter.send   s      	''00	&&y11 ... DD4#4DDDT4466
 $ 
(=(=)))33 )> )
 )
	& C C#,,..	##FD,=>>>##D'222##FE222C&||Ci|QQ S S S S S S SUY%)YY[[000000;#--(,CII(F(FI"LL @ ,W 5 5 $ )	   $.(-&J&J&Jy&J&J$ $ $S S S S S S S S S S S S S S S* % &' &mmoo%%%%%%%%& & #'((5""5"5&0'&R&R&RS S S S S S S S S S S S S S S S S S S S S S S S S S S  ! C C CLL!=|G?T?TVWXXX%e3q66BBBBBBBBBB % &' &mmoo%%%%%%%%& &C/C8 % &' &mmoo%%%%%%% % &' &mmoo%%%%%%%%& & su   A K =H4BH!.H4 K ('H!H4!
H+	+H4.H+	/H42K 4
J6>AJ1J6K 1J66K 'Lc                    K   |ddS )Ndm)r   type )r;   r{   s     r   get_chat_infozSmsAdapter.get_chat_info   s      ...r=   c                      t          |          S )8   Strip markdown — SMS renders it as literal characters.)r   )r;   r|   s     r   r   zSmsAdapter.format_message   s    g&&&r=   r   post_params	signaturec                     |                      |||          rdS |                     |          }|r|                      |||          rdS dS )a  Validate ``X-Twilio-Signature`` header (HMAC-SHA1, base64).

        Tries both with and without the default port for the URL scheme,
        since Twilio may sign with either variant.

        Algorithm: https://www.twilio.com/docs/usage/security#validating-requests
        TF)_check_signature_port_variant_url)r;   r   r   r   variants        r   _validate_twilio_signaturez%SmsAdapter._validate_twilio_signature   sa       k9== 	4((-- 	t,,Wk9MM 	4ur=   c                    |}t          |                                          D ]}||||         z   z  }t          j        | j                            d          |                    d          t          j                  }t          j	        |
                                                              d          }t          j        |                                |                                          S )z.Compute and compare a single Twilio signature.utf-8)sortedkeyshmacnewr/   rD   hashlibsha1rB   rC   digestrE   compare_digest)r;   r   r   r   data_to_signkeymaccomputeds           r   r   zSmsAdapter._check_signature  s     +**,,-- 	3 	3CC+c"222LLh##G,,((L
 

 #CJJLL1188AA "8??#4#4i6F6F6H6HIIIr=   c                    t           j                            |           }ddd}|                    |j                  }|dS |j        |k    rCt           j                            |j        |j        |j        |j	        |j
        |j        f          S |j        J|j         d| }t           j                            |j        ||j        |j	        |j
        |j        f          S dS )zReturn the URL with the default port toggled, or None.

        Only toggles default ports (443 for https, 80 for http).
        Non-standard ports are never modified.
        i  P   )httpshttpNr?   )urllibparseurlparser   schemeport
urlunparsehostnamepathparamsqueryfragment)r   parseddefault_portsdefault_portnetlocs        r   r   zSmsAdapter._port_variant_url   s     &&s++"%r22$((774;,&&<**fo?   [ 88,88F<**fo?   tr=   aiohttp.web.Responsec                   K   ddl m} 	 |j        }|#|t          k    r|                    ddd          S |                                 d {V }t          |          t          k    r|                    ddd          S t          j        	                    |
                    d          d	          }nJ# t          $ r=}t                              d
|           |                    ddd          cY d }~S d }~ww xY w| j        r|j                            dd          }|s2t                              d           |                    ddd          S d |                                D             }|                     | j        ||          s2t                              d           |                    ddd          S |                    ddg          d                                         }	|                    ddg          d                                         }
|                    ddg          d                                         }|                    ddg          d                                         }|	r|s|                    dd          S |	| j        k    r?t                              dt/          |	                     |                    dd          S t                              dt/          |	          t/          |
          |d d                    |                     |	|	d|	|	          }t5          |t6          j        |||          }t;          j        |                     |                    }| j         !                    |           |"                    | j         j#                   |                    dd          S )Nr   rK   z;<?xml version="1.0" encoding="UTF-8"?><Response></Response>zapplication/xmli  )rU   content_typer   r   T)keep_blank_valuesz[sms] webhook parse error: %sr   zX-Twilio-Signaturer&   z1[sms] Rejected: missing X-Twilio-Signature headeri  c                 *    i | ]\  }}|||d          S )r   r   ).0kvs      r   
<dictcomp>z.SmsAdapter._handle_webhook.<locals>.<dictcomp>h  s'    AAAtq!qA1adAAAr=   z([sms] Rejected: invalid Twilio signaturer   r   r   
MessageSid)rU   r   z&[sms] ignoring echo from own number %sz[sms] inbound from %s -> %s: %sr   r   )r{   	chat_name	chat_typeuser_id	user_name)rU   message_typesourceraw_messager   )$r   rL   content_lengthre   rV   readlenr   r   parse_qsrE   r   r_   r`   r8   r   r   rc   itemsr   r7   r0   debugr   rq   build_sourcer	   r
   TEXTasynciocreate_taskhandle_message_background_tasksaddadd_done_callbackdiscard)r;   requestrL   r   rawformr   
twilio_sigflat_paramsfrom_number	to_numberrU   message_sidr   eventtasks                   r   rh   zSmsAdapter._handle_webhookB  sF     	$3N)n?].].]||V!2 $   
  &&&&&&C3xx888||V!2 $    <((G)<)<PT(UUDD 	 	 	LL8!<<<<<R.          	  	 ,,-A2FFJ RSSS||V!2 $   
 BAtzz||AAAK22!;
   IJJJ||V!2 $    xx--q17799XXdRD))1-3355	"&&*0022xxrd33Q7==?? 	$ 	<<R.      $+++LLA<P[C\C\]]]<<R.     
 	-%%##"I		
 	
 	
 ""!! # 
 
 $)"
 
 
 "4#6#6u#=#=>>""4(((t5=>>> ||N*  
 
 	
s)   +B5 A	B5  4B5 5
C<?2C71C<7C<r   N)NN)r   r   )__name__
__module____qualname____doc__MAX_SMS_LENGTHMAX_MESSAGE_LENGTHr   r,   r2   rH   r    rv   rz   r   r   r   r   r   r   r   dictr   r   staticmethodr   rh   __classcell__)r<   s   @r   r#   r#   Q   s,         (E~ E E E E E E"C " " " " 5: 7 7 7T 7d 7 7 7 7r* * * * #'-16 66 6 3-	6
 4S>*6 
6 6 6 6p/3 /4S> / / / /'c 'c ' ' ' '%)69	   &JJ%)J69J	J J J J" s sTz    \Bd
 d
 d
 d
 d
 d
 d
 d
r=   r#   r   c                    t                               dd| t           j                  } t                               dd| t           j                  } t                               dd| t           j                  } t                               dd| t           j                  } t                               dd|           } t                               d	d|           } t                               d
d| t           j                  } t                               dd|           } t                               dd|           } |                                 S )r   z\*\*(.+?)\*\*z\1)flagsz	\*(.+?)\*z	__(.+?)__z_(.+?)_z```[a-z]*\n?r&   z`(.+?)`z
^#{1,6}\s+z\[([^\]]+)\]\([^\)]+\)z\n{3,}z

)resubDOTALL	MULTILINEr7   )r   s    r   _strip_markdown_for_smsr    s    ff%ugRYfGGGff\5'fCCGff\5'fCCGffZryfAAGff_b'22GffZ00Gff]Br|fDDGff.w??GffY00G==??r=   F)	thread_idmedia_filesforce_documentc          	      H  K   t          | dd          pt          dd          }	 ddl}n# t          $ r ddicY S w xY wddl}t          dd          }	t          j        d	d          }
|	r|r|
sdd
iS t          |          }d }	 ddlm	}m
}  |            } ||          \  }}|	 d| } |j        |                    d                                        d          }d|	 d}dd| i} |j        d#d|                    d          i|4 d{V }|                                }|                    d|
           |                    d|           |                    d|            |j        |f||d|4 d{V }|                                 d{V }|j        dk    r]|                    dt-          |                    } |d|j         d|           cddd          d{V  cddd          d{V  S dd||                    d d          d!cddd          d{V  cddd          d{V  S # 1 d{V swxY w Y   	 ddd          d{V  dS # 1 d{V swxY w Y   dS # t.          $ r} |d"|           cY d}~S d}~ww xY w)$zOut-of-process SMS delivery via the Twilio REST API. Implements the
    standalone_sender_fn contract; replaces the legacy _send_sms helper.api_keyNr   r&   r   r`   z/aiohttp not installed. Run: pip install aiohttpr   r'   zXSMS not configured (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER required)c                 N    	 ddl m}  ||           S # t          $ r d| icY S w xY w)Nr   )_errorr`   )tools.send_message_toolr  r   )rU   _es     r   _redacted_errorz)_standalone_send.<locals>._redacted_error  sQ    	#<<<<<<2d88O 	# 	# 	#T?"""	#s    $$)resolve_proxy_urlproxy_kwargs_for_aiohttpr?   r@   z+https://api.twilio.com/2010-04-01/Accounts/r   r   rA   r]   rY   rZ   r   r   r   r   r   r   zTwilio API error (z): Tsmsr   )r   platformr{   r   zSMS send failed: r   )getattrr   r   r   rB   r   r   r  gateway.platforms.baser  r  rC   rD   rE   rn   ro   r   r   r   r   r   r   r2   r   )pconfigr{   r   r  r  r  
auth_tokenr   rB   account_sidr   r  r  r  _proxy_sess_kw_req_kwrF   rG   r   r   r   r   r   r   r   r   s                              r   _standalone_sendr%    s      )T22a6HI\^`6a6aJL L L LJKKKKLMMM$%92>>K)1266K uj u ustt%g..G# # #8VVVVVVVV""$$44V<<'----"&"5<<#8#899@@IIWKWWW"$6W$6$67(7(]]1F1FR1F1P1P]T\]] 
	s 
	s 
	s 
	s 
	s 
	s 
	sah((**I444g...000#w|CTiTTGTT s s s s s s sX\!YY[[((((((;#%% $CII > >I*?+[+[+[PY+[+[\\	s s s s s s s s s s s s s
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s $(Uw^b^f^fglnp^q^qrrs s s s s s s s s s s s s
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s
s s s s s s s s s s s s s s s
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s 
	s  8 8 86166777777778s   * ;;BI? A.I,AI#I,5I? I"I,4I? 
I	I,I	I,I? ,
I66I? 9I6:I? ?
J!	JJ!J!c                 z    ddl m} t          |                    d          pd                                          S )zSMS is connected when Twilio credentials are present. Mirrors the legacy
    _PLATFORM_CONNECTED_CHECKERS[Platform.SMS] = bool(TWILIO_ACCOUNT_SID).r   Nr   r&   )hermes_cli.gatewaygatewayr    get_env_valuer7   )r$   gateway_mods     r   _is_connectedr+    sD     -,,,,,**+?@@FBMMOOPPPr=   c                      t          |           S )zAFactory wrapper that constructs SmsAdapter from a PlatformConfig.)r#   )r$   s    r   _build_adapterr-    s    fr=   c                     |                      ddt          t          t          g dddddt          t
          dd	d
           dS )u:   Plugin entry point — called by the Hermes plugin system.r  zSMS (Twilio))r   r   r'   zpip install aiohttpSMS_ALLOWED_USERSSMS_ALLOW_ALL_USERSSMS_HOME_CHANNELTu   📱)r   labeladapter_factorycheck_fnis_connectedrequired_envinstall_hintallowed_users_envallow_all_envcron_deliver_env_varstandalone_sender_fnmax_message_lengthpii_safeemojiallow_update_commandN)register_platformr-  r!   r+  r%  r  )ctxs    r   registerrB    s\    &'"WWW*-+/-)!      r=   )Nr   )/r  r   rB   r   r   loggingr   urllib.parser   typingr   r   r   gateway.configr   r   r  r   r	   r
   r   gateway.platforms.helpersr   r   agent.secret_scoper   r   r   r   r   	getLoggerr   r_   r   r  r3   r5   re   r    r!   r#   r2   r  r%  r+  r-  rB  r   r=   r   <module>rJ     s0   (      				     & & & & & & & & & & 3 3 3 3 3 3 3 3            C B B B B B B B J J J J J J ? ? ? ? ? ?/ / / /( 
	8	$	$> " !' f f f f fU
 U
 U
 U
 U
$ U
 U
 U
HS S    & 48 48 48 48 48nQT Q Q Q Q  
     r=   