
    (GjL                         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  ej        e          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_markdownz*https://api.twilio.com/2010-04-01/Accountsi@  i  z	127.0.0.1returnc                      	 ddl } n# t          $ r Y dS w xY wt          t          j        d          ot          j        d                    S )z0Check if SMS adapter dependencies are available.r   NFTWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN)aiohttpImportErrorboolosgetenv)r   s    C/home/rurouni/.hermes/hermes-agent/plugins/platforms/sms/adapter.pycheck_sms_requirementsr   /   s]       uu	.//RBI>Q4R4RSSSs    
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          j        d         | _        t          j        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   environ_account_sid_auth_tokenr   _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__B   s    ...!#,@!A "
+> ?!#+@"!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)r3   credsencodeds      r   _basic_auth_headerzSmsAdapter._basic_auth_headerO   sS    $99t'799"5<<#8#899@@II!!!!r5   F)is_reconnectrA   c                6  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                                                   }|j                            d| j                   |j                            dfd                               |          | _        | j                                         d {V                      | j        | j        | j                  }|                                 d {V  |                    |                    d          d          | _        d| _        t                              d| j        | j        t9          | 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.z/webhooks/twilioz/healthc                 0                         d          S )Nok)text)Response)_rD   s    r   <lambda>z$SmsAdapter.connect.<locals>.<lambda>{   s    $0G0G r5      totalTtimeout	trust_envz8[sms] Twilio webhook server listening on %s:%d, from: %s)r   rD   r(   loggererror_set_fatal_errorr   r   lowerr0   warningr,   Applicationrouteradd_post_handle_webhookadd_get	AppRunnerr1   setupTCPSiter.   startClientSessionClientTimeoutr2   _runninginfor   )r3   rA   r   msginsecure_no_sigappsiterD   s          @r   connectzSmsAdapter.connectY   sL       	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
.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*++		
 	
 	
 tr5   Nc                    K   | j         r&| j                                          d {V  d | _         | j        r&| j                                         d {V  d | _        d| _        t
                              d           d S )NFz[sms] Disconnected)r2   closer1   cleanuprf   rV   rg   )r3   s    r   
disconnectzSmsAdapter.disconnect   s       	&$**,,,,,,,,,!%D< 	 ,&&(((((((((DL()))))r5   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AuthorizationrP   rQ   rS   FromToBodydataheaders  messagez[sms] send failed to %s: %s %sFzTwilio z: )rv   rW   sidr   )rv   
message_idz[sms] send error to %s: %s)r   format_messagetruncate_messager   TWILIO_API_BASEr&   r@   r2   rd   re   FormData	add_fieldr(   postjsonstatusgetr*   rV   rW   r   rn   	Exception)r3   rq   rr   rs   rt   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)nametype )r3   rq   s     r   get_chat_infozSmsAdapter.get_chat_info   s      ...r5   c                      t          |          S )8   Strip markdown — SMS renders it as literal characters.)r   )r3   rr   s     r   r   zSmsAdapter.format_message   s    g&&&r5   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)r3   r   r   r   variants        r   _validate_twilio_signaturez%SmsAdapter._validate_twilio_signature   sa       k9== 	4((-- 	t,,Wk9MM 	4ur5   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'   r<   hashlibsha1r:   r;   digestr=   compare_digest)r3   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"8Y777r5   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httpNr7   )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?   tr5   aiohttp.web.Responsec                   K   ddl m} 	 |                                 d {V }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||          }t/          |
t0          j        |||          }t5          j        |                     |                    }| j                            |           |                    | j        j                    |
                    dd          S )Nr   rC   r   T)keep_blank_valuesz[sms] webhook parse error: %sz;<?xml version="1.0" encoding="UTF-8"?><Response></Response>zapplication/xmlr   )rL   content_typer   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>=  s'    AAAtq!qA1adAAAr5   z([sms] Rejected: invalid Twilio signaturerz   r{   r|   
MessageSid)rL   r   z&[sms] ignoring echo from own number %sz[sms] inbound from %s -> %s: %sr   r   )rq   	chat_name	chat_typeuser_id	user_name)rL   message_typesourceraw_messager   )!r   rD   readr   r   parse_qsr=   r   rV   rW   rM   r0   r   r   rZ   itemsr   r/   r(   debugr   rg   build_sourcer	   r
   TEXTasynciocreate_taskhandle_message_background_tasksaddadd_done_callbackdiscard)r3   requestrD   rawformr   
twilio_sigflat_paramsfrom_number	to_numberrL   message_sidr   eventtasks                  r   r^   zSmsAdapter._handle_webhook$  s     
	&&&&&&C<((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   AA 
B #2BB B r   N)NN)r   r   )__name__
__module____qualname____doc__MAX_SMS_LENGTHMAX_MESSAGE_LENGTHr   r#   r*   r@   r   rl   rp   r   r   r   r   r   r   r   dictr   r   staticmethodr   r^   __classcell__)r4   s   @r   r   r   8   s$         (E~ E E E E E E"C " " " " 5: 4 4 4T 4d 4 4 4 4l* * * * #'-16 66 6 3-	6
 4S>*6 
6 6 6 6p/3 /4S> / / / /'c 'c ' ' ' '%)69	   &88%)8698	8 8 8 8 s sTz    \BW
 W
 W
 W
 W
 W
 W
 W
r5   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	MULTILINEr/   )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==??r5   F)	thread_idmedia_filesforce_documentc          	      \  K   t          | dd          pt          j        dd          }	 ddl}n# t          $ r ddicY S w xY wddl}t          j        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   rW   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   )_errorrW   )tools.send_message_toolr
  r   )rL   _es     r   _redacted_errorz)_standalone_send.<locals>._redacted_error  sQ    	#<<<<<<2d88O 	# 	# 	#T?"""	#s    $$)resolve_proxy_urlproxy_kwargs_for_aiohttpr7   r8   z+https://api.twilio.com/2010-04-01/Accounts/rx   ry   r9   rT   rP   rQ   rz   r{   r|   r}   r   r   zTwilio API error (z): Tsmsr   )rv   platformrq   r   zSMS send failed: r   )getattrr   r   r   r   r:   r  gateway.platforms.baser  r  r;   r<   r=   rd   re   r   r   r   r   r   r   r*   r   )pconfigrq   r   r  r  r  
auth_tokenr   r:   account_sidr   r  r  r  _proxy_sess_kw_req_kwr>   r?   r   r   r   r   r   r   r   r   s                              r   _standalone_sendr    s      )T22Xbi@SUW6X6XJL L L LJKKKKLMMM)0"55K)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   / A A BJ	 !A.I6AI-I6?J	 I,I6>J	 
I	I6I	I6#J	 6
J  J	 J J	 	
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_valuer/   )r   gateway_mods     r   _is_connectedr     sD     -,,,,,**+?@@FBMMOOPPPr5   c                      t          |           S )zAFactory wrapper that constructs SmsAdapter from a PlatformConfig.)r   )r   s    r   _build_adapterr"    s    fr5   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   registerr7    s\    &'"WWW*-+/-)!      r5   r   )(r   r   r:   r   r   loggingr   urllib.parser   typingr   r   r   gateway.configr   r   r  r   r	   r
   r   gateway.platforms.helpersr   r   	getLoggerr   rV   r   r   r+   r-   r   r   r   r*   r  r  r   r"  r7  r   r5   r   <module>r>     s   (      				     & & & & & & & & & & 3 3 3 3 3 3 3 3            C B B B B B B B		8	$	$> " T T T T TC
 C
 C
 C
 C
$ C
 C
 C
d
S S    & 48 48 48 48 48nQT Q Q Q Q  
     r5   