
    `gjqQ                         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j0                  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(y)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.1i   returnc                      	 ddl } t        t        j                  d      xr t        j                  d            S # t        $ r Y yw xY w)z0Check if SMS adapter dependencies are available.r   NFTWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKEN)aiohttpImportErrorboolosgetenv)r   s    P/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/sms/adapter.pycheck_sms_requirementsr   0   sD     		./RBII>Q4RSS  s   ; 	AA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      j!                         | _        d | _        d | _        y )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__C   s    .!#,@!A "

+> ?!#+@"!E"%II(#.B*CD#
 #%)),>@T"U!#+<b!A!G!G!I@D    r   c                     | j                    d| j                   }t        j                  |j	                  d            j                  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_headerP   sQ    $$%Qt'7'7&89""5<<#89@@Iy!!r5   F)is_reconnectrA   c                L  K   dd l }ddl m | j                  s,d}t        j	                  |       | j                  d|d       yt        j                  dd      j                         d	k(  }| j                  s.|s,d
}t        j	                  |       | j                  d|d       y|r,| j                  s t        j                  d| j                         j                  t              }|j                  j                  d| j                          |j                  j#                  dfd       j%                  |      | _        | j&                  j)                          d {    j+                  | j&                  | j,                  | j                        }|j/                          d {    |j1                  |j3                  d      d      | _        d| _        t        j9                  d| j,                  | j                  t;        | j                               y7 7 uw)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                 (    j                  d      S )Nok)text)Response)_rD   s    r   <lambda>z$SmsAdapter.connect.<locals>.<lambda>   s    $0G 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_TWILIO_WEBHOOK_MAX_BODY_BYTES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.connectZ   s      MCLL!!"<cU!S))$?DJJLPVV  S  LL!!";SE!R4#4#4NN1 ""	 oo.LoM

.0D0DE

9&GH}}S)ll  """{{4<<););T=O=OPjjl$22)))3 3 
 F**+		
  	#s&   E H$#H $AH$,H"-A4H$"H$Nc                    K   | j                   r)| j                   j                          d {    d | _         | j                  r)| j                  j                          d {    d | _        d| _        t
        j                  d       y 7 ]7 *w)NFz[sms] Disconnected)r2   closer1   cleanuprh   rW   ri   )r3   s    r   
disconnectzSmsAdapter.disconnect   sr     $$**,,,!%D<<,,&&(((DL() - )s!   *BB
4B!B")BBchat_idcontentreply_tometadatac           	        K   dd l }| j                  |      }| j                  |      }t        d      }t         d| j
                   d}	d| j                         i}
| j                  xs# |j                  |j                  d      d	      }	 |D ]e  }|j                         }|j                  d
| j                         |j                  d|       |j                  d|       	 |j                  |	||
      4 d {   }|j                          d {   }|j                  dk\  r|j!                  dt#        |            }t$        j'                  dt)        |      |j                  |       t        dd|j                   d|       cd d d       d {    c | j                  s|r|j+                          d {    S S S |j!                  dd      }t        d|      }d d d       d {    h 	 | j                  s|r|j+                          d {    |S 7 %7 7 7 j7 :# 1 d {  7  sw Y   JxY w# t,        $ rk}t$        j'                  dt)        |      |       t        dt#        |            cY d }~c | j                  s|r|j+                          d {  7   S S S d }~ww xY w7 # | j                  s|r|j+                          d {  7   w w w xY ww)Nr   T)success//Messages.jsonAuthorizationrQ   rR   rT   FromToBodydataheaders  messagez[sms] send failed to %s: %s %sFzTwilio z: )rx   rX   sidr   )rx   
message_idz[sms] send error to %s: %s)r   format_messagetruncate_messager   TWILIO_API_BASEr&   r@   r2   rf   rg   FormData	add_fieldr(   postjsonstatusgetr*   rW   rX   r   rp   	Exception)r3   rs   rt   ru   rv   r   	formattedchunkslast_resulturlr   sessionchunk	form_datarespbody	error_msgmsg_sides                      r   sendzSmsAdapter.send   s     	''0	&&y1 . !4#4#4"5^DT446
 $$ 
(=(=)))3 )> )
	& C#,,.	##FD,=,=>##D'2##FE2C&||Ci|Q S SUY%)YY[0;;#-(,CI(FI"LL @ ,W 5 $ )	 $.(-(/}Byk&J$S S S* %%'mmo%% +2% #'((5""5&0'&RS SC8 %%'mmo%%1S0S, &-S S S S  ! CLL!=|G?TVWX%e3q6BB %%'mmo%% +2%C & %%'mmo%% +2%s  BK+AJ= &I>H#?IH/H&A5H/IH)IJ= !K+ H+K+H/'I2H-3I7J= ;!K+J;K+#I&H/)I+K+-I/I	5H86I	=I	J85J3J8J= 	!K+*J-+K+3J88J= ;K+="K(K" K((K+c                    K   |ddS w)Ndm)nametype )r3   rs   s     r   get_chat_infozSmsAdapter.get_chat_info   s     ..s   c                     t        |      S )8   Strip markdown — SMS renders it as literal characters.)r   )r3   rt   s     r   r   zSmsAdapter.format_message   s    g&&r5   r   post_params	signaturec                 z    | j                  |||      ry| j                  |      }|r| j                  |||      ryy)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   sC       k9=((-t,,Wk9Mr5   c                    |}t        |j                               D ]  }||||   z   z  } t        j                  | j                  j                  d      |j                  d      t        j                        }t        j                  |j                               j                  d      }t        j                  |j                         |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     +**,- 	3CC+c"222L	3hh##G,(LL

 ##CJJL188A ""8??#4i6F6F6HIIr5   c                 P   t         j                  j                  |       }ddd}|j                  |j                        }|y|j
                  |k(  rat         j                  j                  |j                  |j                  |j                  |j                  |j                  |j                  f      S |j
                  h|j                   d| }t         j                  j                  |j                  ||j                  |j                  |j                  |j                  f      S y)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$((7;;,&<<**foo?  [[ (,8F<<**foo?  r5   c                   K   ddl m} 	 |j                  }||t        kD  r|j	                  ddd      S |j                          d {   }t        |      t        kD  r|j	                  ddd      S t        j                  j                  |j                  d      d	      }| j                  r|j                  j!                  dd      }|s)t        j#                  d       |j	                  ddd      S |j%                         D 	ci c]  \  }}	|	s	||	d    }
}}	| j'                  | j                  |
|      s)t        j#                  d       |j	                  ddd      S |j!                  ddg      d   j)                         }|j!                  ddg      d   j)                         }|j!                  ddg      d   j)                         }|j!                  ddg      d   j)                         }|r|s|j	                  dd      S || j*                  k(  r2t        j-                  dt/        |             |j	                  dd      S t        j1                  dt/        |      t/        |      |d d        | j3                  ||d||      }t5        |t6        j8                  |||      }t;        j<                  | j?                  |            }| j@                  jC                  |       |jE                  | j@                  jF                         |j	                  dd      S 7 # t        $ r4}t        j                  d
|       |j	                  ddd      cY d }~S d }~ww xY wc c}	}w w)Nr   rC   z;<?xml version="1.0" encoding="UTF-8"?><Response></Response>zapplication/xmli  )rM   content_typer   r   T)keep_blank_valuesz[sms] webhook parse error: %sr   zX-Twilio-Signaturer   z1[sms] Rejected: missing X-Twilio-Signature headeri  z([sms] Rejected: invalid Twilio signaturer|   r}   r~   
MessageSid)rM   r   z&[sms] ignoring echo from own number %sz[sms] inbound from %s -> %s: %sr   r   )rs   	chat_name	chat_typeuser_id	user_name)rM   message_typesourceraw_messager   )$r   rD   content_lengthr]   rN   readlenr   r   parse_qsr=   r   rW   rX   r0   r   r   r[   itemsr   r/   r(   debugr   ri   build_sourcer	   r
   TEXTasynciocreate_taskhandle_message_background_tasksaddadd_done_callbackdiscard)r3   requestrD   r   rawformr   
twilio_sigkvflat_paramsfrom_number	to_numberrM   message_sidr   eventtasks                     r   r`   zSmsAdapter._handle_webhook*  ss    	$33N)n?].]||V!2 $  
  &C3x88||V!2 $   <<((G)<PT(UD  ,,-A2FJRS||V!2 $  
 04zz|Atq!q1ad7AKA22!!;
 IJ||V!2 $   xx-q1779XXdRD)1-335	"&*002xxrd3Q7==?$<<R.     $+++LLA<P[C\]<<R.    
 	-%#"I		
 ""!! # 
 $))"
 ""4#6#6u#=>""4(t55==> ||N*  
 	
m '  	LL8!<<<R.    	$ Bsp   M.*L( M.L( L%	)L( 2M.30L( #A&M.	
M(M(H	M.%L( (	M%1)M M%M. M%%	M.r   N)NN)r   zaiohttp.web.Response)__name__
__module____qualname____doc__MAX_SMS_LENGTHMAX_MESSAGE_LENGTHr   r#   r*   r@   r   rn   rr   r   r   r   r   r   r   r   dictr   r   staticmethodr   r`   __classcell__)r4   s   @r   r   r   9   sA    (E~ E"C " 5: 7T 7d 7r* #'-166 6 3-	6
 4S>*6 
6p/3 /4S> /'c 'c '%)69	&JJ%)J69J	J" s sTz  Bd
r5   r   r   c                 `   t         j                  dd| t         j                        } t         j                  dd| t         j                        } t         j                  dd| t         j                        } t         j                  dd| t         j                        } t         j                  dd|       } t         j                  d	d|       } t         j                  d
d| t         j                        } t         j                  dd|       } t         j                  dd|       } | j	                         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%ugRYYfGGff\5'fCGff\5'fCGffZryyfAGff_b'2GffZ0Gff]Br||fDGff.w?GffY0G==?r5   F)	thread_idmedia_filesforce_documentc          	      8  K   t        | dd      xs t        j                  dd      }	 ddl}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                  |j                  d            j                  d      }d|	 d}dd| i} |j                  d#d|j                  d      i|4 d{   }|j                         }|j!                  d|
       |j!                  d|       |j!                  d|        |j"                  |f||d|4 d{   }|j%                          d{   }|j&                  dk\  rU|j)                  dt+        |            } |d|j&                   d|       cddd      d{    cddd      d{    S dd||j)                  d d      d!cddd      d{    cddd      d{    S # t        $ r ddicY S w xY w7 27 7 7 i7 Z7 47 %# 1 d{  7  sw Y   nxY wddd      d{  7   y# 1 d{  7  sw Y   yxY w# t,        $ r} |d"|       cY d}~S d}~ww xY w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   rX   z/aiohttp not installed. Run: pip install aiohttpr   r   zXSMS not configured (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER required)c                 F    	 ddl m}  ||       S # t        $ r d| icY S w xY w)Nr   )_errorrX   )tools.send_message_toolr
  r   )rM   _es     r   _redacted_errorz)_standalone_send.<locals>._redacted_error  s,    	#<d8O 	#T?"	#s      )resolve_proxy_urlproxy_kwargs_for_aiohttpr7   r8   z+https://api.twilio.com/2010-04-01/Accounts/rz   r{   r9   rU   rQ   rR   r|   r}   r~   r   r   r   zTwilio API error (z): Tsmsr   )rx   platformrs   r   zSMS send failed: r   )getattrr   r   r   r   r:   r  gateway.platforms.baser  r  r;   r<   r=   rf   rg   r   r   r   r   r   r   r*   r   )pconfigrs   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Xbii@SUW6XJL ))0"5K))126Kjstt%g.G#8V"$4V<'-q-"&""5<<#89@@I;K=W"fWI$67(7((]1F1FR1F1P]T\] 
	s 
	sah((*I4g.0#w||CTiTGT s sX\!YY[(;;#% $CI >I*-?}CPY{+[\	s s s
	s 
	s 
	s $(Uw^b^f^fglnp^qrs s s
	s 
	s 
	s5  LJKKL4
	s
s(s
	s
s
	s
s s s
	s 
	s 
	s 
	s 
	s  8!21#6778s]  &JH AJ6BI9 <H.=I9  A!I$!H1"I$%H=9H3:AH=?I$H5I$I9 H7I9 !J"H=8I$H9I$	I9 H;I9 JH+(J*H++J.I9 1I$3H=5I$7I9 9I$;I9 =I	II	I$I9 I I9 #J$I6*I-+I62I9 5J6I9 9	J
JJJJJc                 f    ddl m} t        |j                  d      xs dj	                               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     s,     -**+?@FBMMOPPr5   c                     t        |       S )zAFactory wrapper that constructs SmsAdapter from a PlatformConfig.)r   )r   s    r   _build_adapterr"    s    fr5   c                 p    | j                  ddt        t        t        g dddddt        t
        dd	d
       y)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    sB    &'"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   rW   r   r   r+   r-   r]   r   r   r   r*   r  r  r   r"  r7  r   r5   r   <module>r>     s   (      	  & & 3  C			8	$> " !' T TU
$ U
HS S & 48nQT Q
r5   