
    ,j                         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Zddl	m	Z	m
Z
 ddl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mZmZmZ  ej        e          ZdZd	Zd
ZdZ dZ!dZ"de#dee#         fdZ$de#de#fdZ%de&de#fdZ'de#de(fdZ)de#de(fdZ* G d de          Z+de(fdZ,de(fdZ-de(fdZ.dee/         fdZ0ddddd e#d!e#d"ee#         d#eee#                  d$e(dee#ef         fd%Z1d(d&Z2d(d'Z3dS ))uP	  SimpleX Chat platform adapter (Hermes plugin).

Connects to a simplex-chat daemon running in WebSocket mode.
Inbound messages arrive via a persistent WebSocket connection.
Outbound messages use the same WebSocket with JSON commands.

This adapter ships as a Hermes platform plugin under
``plugins/platforms/simplex/``. The Hermes plugin loader scans the
directory at startup, calls ``register(ctx)``, and the platform
becomes available to ``gateway/run.py`` and ``tools/send_message_tool``
through the registry — no edits to core files are required.

SimpleX chat daemon setup:
    simplex-chat -p 5225          # start daemon on port 5225
    # or via Docker:
    # docker run -p 5225:5225 simplexchat/simplex-chat-cli -p 5225

Required environment variables:
    SIMPLEX_WS_URL             WebSocket URL of the daemon
                               (default: ws://127.0.0.1:5225)

Optional environment variables:
    SIMPLEX_ALLOWED_USERS      Comma-separated allowlist. Each entry may be
                               either a numeric contactId (stable across
                               renames; visible via `/contacts` in the CLI)
                               or a contact display name (what the SimpleX
                               UI shows). Both forms are accepted.
    SIMPLEX_ALLOW_ALL_USERS    Set 'true' to allow all contacts
    SIMPLEX_AUTO_ACCEPT        Set 'false' to disable contact-request auto-accept
                               (default: 'true')
    SIMPLEX_GROUP_ALLOWED      Comma-separated group IDs to monitor, or '*'
                               for any group. Omit to disable groups entirely.
    SIMPLEX_HOME_CHANNEL       Default contact/group ID for cron delivery
    SIMPLEX_HOME_CHANNEL_NAME  Human label for the home channel
    HERMES_SIMPLEX_TEXT_BATCH_DELAY
                               Quiet-period seconds (default: 0.8) used to
                               concatenate rapid-fire inbound text messages
                               into a single MessageEvent — same pattern as
                               Telegram's text batching.

The ``websockets`` Python package is imported lazily — the plugin is
discoverable and ``hermes setup`` can describe it even when websockets is
not installed. ``check_requirements()`` returns False until the package
is present, so the gateway will not attempt to instantiate the adapter.
    N)datetimetimezone)Path)AnyDictListOptional)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResulti@  g       @g      N@      >@g     r@zhermes-valuereturnc                 @    d |                      d          D             S )z4Split a comma-separated string into a stripped list.c                 ^    g | ]*}|                                 |                                 +S  )strip).0vs     F/home/ubuntu/.hermes/hermes-agent/plugins/platforms/simplex/adapter.py
<listcomp>z%_parse_comma_list.<locals>.<listcomp>[   s-    ===!17799=AGGII===    ,)split)r   s    r   _parse_comma_listr   Y   s"    ==u{{3//====r   
contact_idc                     | sdS t          |           }t          |          dk    r|S |dd         dz   |dd         z   S )z&Redact a contact/group ID for logging.z<none>   N   z**)strlen)r   ss     r   
_redact_idr'   ^   sM     xJA
1vv{{RaR54<!BCC&  r   datac                    | dd         dk    rdS | dd         dk    rdS | dd         dk    rd	S t          |           d
k    r| dd         dk    r| dd
         dk    rdS | dd         dk    rdS t          |           dk    r| dd         dk    rdS | dd         dk    rdS t          |           dk    r| d         dk    r| d         dz  dk    rdS dS )z&Guess file extension from magic bytes.Nr!   s   PNG.pngr"   s   .jpgs   GIF8.gif   s   RIFF   s   WEBP.webps   %PDFz.pdfs   ftypz.mp4s   OggS.oggr            .mp3z.bin)r%   )r(   s    r   _guess_extensionr5   h   s   BQBx:vBQBx;vBQBx7v
4yyB48w..4":3H3HwBQBx7v
4yyA~~$qs)w..vBQBx7v
4yyA~~$q'T//tAw~$.F.Fv6r   extc                 .    |                                  dv S )N>   r,   r+   r*   .jpegr/   lowerr6   s    r   _is_image_extr<   }   s    99;;DDDr   c                 .    |                                  dv S )N>   .aac.m4ar4   r0   .wav.opusr9   r;   s    r   _is_audio_extrB      s    99;;KKKr   c                       e Zd ZdZeZdef fdZdefdZd0dZ	d0dZ
d0d	Zd
eddfdZdeddfdZd
edefdZd
eddfdZdeddfdZdefdZdeddfdZ	 d1dededee         fdZdeddfdZ	 	 d2dededee         deeeef                  def
dZededeeef         fd             Z 	 d3ded!ed"ee         defd#Z!	 	 d2ded$ed"ee         dee         def
d%Z"	 	 d2ded&ed"ee         dee         def
d'Z#	 	 d2deded"ee         d(ee         def
d)Z$	 	 	 d4ded+ed"ee         dee         d,e%defd-Z&d3deddfd.Z'dedeeef         fd/Z( xZ)S )5SimplexAdapterzSimpleX Chat adapter using the simplex-chat daemon WebSocket API.

    Instantiated by the ``adapter_factory`` passed to
    ``ctx.register_platform()`` in :func:`register`.
    configc                    t          d          }t                                          ||           t          |di           pi }|                    dd                              d          | _        t          j        d          }|.|	                                
                                dv| _        n(t          |                    d	d
                    | _        t          j        dd          p|                    dd          }t          t          |                    | _        d | _        d | _        d | _        d| _        d| _        t                      | _        d| _        i | _        i | _        d| _        t5          t          j        dd                    | _        i | _        i | _        t<                              d| j        | j        | j        rdnd           d S )Nsimplex)rE   platformextraws_urlws://127.0.0.1:5225/SIMPLEX_AUTO_ACCEPT>    0nofalseauto_acceptTSIMPLEX_GROUP_ALLOWEDrN   group_allowedFg           r   HERMES_SIMPLEX_TEXT_BATCH_DELAYz0.8z<SimpleX adapter initialized: url=%s auto_accept=%s groups=%senableddisabled) r
   super__init__getattrgetrstriprJ   osgetenvr   r:   rR   boolsetr   group_allow_from_ws_ws_task_health_task_running_last_ws_activity_pending_corr_ids_max_pending_corr_pending_file_transfers_pending_responses_corr_counterfloat_text_batch_delay_pending_text_batches_pending_text_batch_tasksloggerinfo)selfrE   kwargsrH   rI   env_autogroup_allowed_str	__class__s          r   rZ   zSimplexAdapter.__init__   s   I&&:::,,2ii*?@@GGLL
 9233'~~//5577?WWD#EIImT$B$BCCD
 I&=rBB 
eiiRG
 G
 !$$56G$H$H I I 0448!$ '*ee!$
 9;$
 >@ "'I7??"
 "
 ?A"BD&JK.>IIJ		
 	
 	
 	
 	
r   r   c                 0  K   	 ddl }n+# t          $ r t                              d           Y dS w xY w| j        st                              d           dS 	 ddl }|                    | j        d          4 d{V  	 ddd          d{V  n# 1 d{V swxY w Y   n9# t          $ r,}t                              d| j        |           Y d}~dS d}~ww xY wd	| _        t          j                    | _	        t          j        |                                           | _        t          j        |                                           | _        t!          | d
          r|                                  t                              d| j                   d	S )zDConnect to the simplex-chat daemon and start the WebSocket listener.r   NzHSimpleX: 'websockets' package not installed. Run: pip install websocketsFz#SimpleX: SIMPLEX_WS_URL is required
   )open_timeoutz&SimpleX: cannot reach daemon at %s: %sT_mark_connectedzSimpleX: connected to %s)
websocketsImportErrorrq   errorrJ   connect	Exceptionrf   timerg   asynciocreate_task_ws_listenerrd   _health_monitorre   hasattrr{   rr   )rs   r|   	_wsclientes       r   r   zSimplexAdapter.connect   s7     	 	 	 	LL.   55	 { 	LL>???5	**** ((2(FF                                   	 	 	LLA4;PQRRR55555	 !%+D,=,=,?,?@@#/0D0D0F0FGG4*++ 	#  """.<<<tsH   	 $11&B$ >B B$ 
BB$ B B$ $
C.!CCNc                   K   d| _         | j        r=| j                                         	 | j         d{V  n# t          j        $ r Y nw xY w| j        r=| j                                         	 | j         d{V  n# t          j        $ r Y nw xY w| j        r8	 | j                                         d{V  n# t          $ r Y nw xY wd| _        t          | j
                                                  D ]*}|                                s|                                 +| j
                                         | j                                         | j                                        D ]*}|                                s|                                 +| j                                         t!          | d          r|                                  t$                              d           dS )z%Stop WebSocket listener and clean up.FN_mark_disconnectedzSimpleX: disconnected)rf   rd   cancelr   CancelledErrorre   rc   closer   listrp   valuesdoneclearro   rk   r   r   rq   rr   )rs   taskfuts      r   
disconnectzSimplexAdapter.disconnect   s8     = 	M  """m########)     	$$&&&''''''''')    8 	hnn&&&&&&&&&&   DH 7>>@@AA 	 	D99;; &,,..."((*** *1133 	 	C88:: 

%%'''4-.. 	&##%%%+,,,,,s3   9 A
A/A= =BBB: :
CCc                   K   ddl }ddlm} t          }| j        rR	 t
                              d| j                   |                    | j        ddd          4 d{V 	 }|| _	        t          }t          j
                    | _        t
                              d           |2 3 d{V }| j        s nt          j
                    | _        	 t          j        |          }|                     |           d{V  Y# t          j        $ r t
                              d	|           Y t"          $ r t
                              d
           Y w xY w6 	 ddd          d{V  n# 1 d{V swxY w Y   n# t&          j        $ r Y d| _	        dS |$ r-}| j        rt
                              d||           Y d}~n=d}~wt"          $ r-}| j        rt
                              d||           Y d}~nd}~ww xY wd| _	        n# d| _	        w xY w| j        rN|dz  t-          j                    z  }t'          j        ||z              d{V  t1          |dz  t2                    }| j        PdS dS )z9Maintain a persistent WebSocket connection to the daemon.r   N)ConnectionClosedzSimpleX WS: connecting to %s   ry   )ping_intervalping_timeoutclose_timeoutzSimpleX WS: connectedz SimpleX WS: invalid JSON: %.100sz SimpleX WS: error handling eventz9SimpleX WS: connection closed: %s (reconnecting in %.0fs)z8SimpleX WS: unexpected error: %s (reconnecting in %.0fs)g?r"   )r|   websockets.exceptionsr   WS_RETRY_DELAY_INITIALrf   rq   debugrJ   r   rc   r   rg   rr   jsonloads_handle_eventJSONDecodeErrorr   	exceptionr   r   warningrandomsleepminWS_RETRY_DELAY_MAX)	rs   r   r   backoffwsrawmsgr   jitters	            r   r   zSimplexAdapter._ws_listener#  s     &&&&::::::(m .	?( ;T[III$,,K"$!#"$	 -   Q Q Q Q Q Q Q Q
 !DH4G-1Y[[D*KK 7888%' 
Q 
Q 
Q 
Q 
Q 
Q 
Qc#} "!E15.Q"&*S//C"&"4"4S"9"999999999#3 R R R"LL)KSQQQQQ( Q Q Q",,-OPPPPPQ &(RQ Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q. )      $   = NNS7      = NNR7    4} ? 38mGf$4555555555gk+=>>] m .	? .	? .	? .	? .	?s   AE9 "AE'%E+"E'/C>=E'>*E(E'*#EE'EE'E9 '
E11E9 4E15E9 8H 9G8H G8#F>9H >G8#G3.H 3G88H 	Hc                    K   | j         rrt          j        t                     d{V  | j         sdS t	          j                    | j        z
  }|t          k    rt                              d|           | j         pdS dS )a  Observe WebSocket idleness without reconnecting healthy quiet links.

        simplex-chat can legitimately stay application-silent for long periods
        when no messages arrive. The websockets client already sends protocol
        pings (see _ws_listener ping_interval/ping_timeout), so treating lack of
        chat events as a stale connection causes needless reconnect churn.
        Nz&SimpleX: WS application-idle for %.0fs)	rf   r   r   HEALTH_CHECK_INTERVALr   rg   HEALTH_CHECK_STALE_THRESHOLDrq   r   )rs   elapseds     r   r   zSimplexAdapter._health_monitor^  s       m 	P- 5666666666= ikkD$::G555EwOOO m 	P 	P 	P 	P 	Pr   eventc                 "	  K   t          |                    d          t                    r|                    d          n|}|                    d          }|rN|| j        v rE| j                            |          }|                                s|                    |           dS |rKt          |t                    r6|                    t                    r| j
                            |           dS |                    d          p|                    dd          }|dk    r| j        r|                    di           pi }|                    d          }|St                              dt          t          |                               |                     d	|            d{V  dS |d
k    r|                    di           pi }t          |t                    r|                    d          nd}	|	9t                              d|	           |                     d|	            d{V  dS |dk    r}|                    dg           pg }
t          |
t&                    s|
g}
|
D ]H}	 |                     |           d{V  # t*          $ r t                              d           Y Ew xY wdS |dk    rI	 |                     |           d{V  n*# t*          $ r t                              d           Y nw xY wdS |dk    r`|                    di           pi }|                    di           pi }|                    di           pi }t          |t                    r|                    d          nd}	|	|	| j        v r| j                            |	          }|                    di           pi }t          |t                    r|                    d          nd}|r|                    di           pi }d|i|                    di           d<   ||d<   	 |                     |           d{V  n*# t*          $ r t                              d           Y nw xY wdS |rt                              d|           dS dS )z3Dispatch a daemon event to the appropriate handler.respcorrIdNtyperN   contactRequestcontactRequestIdz*SimpleX: auto-accepting contact request %sz/accept rcvFileDescrReadyrcvFileTransferfileIdu>   SimpleX: rcvFileDescrReady for fileId=%s — sending /freceive
/freceive newChatItems	chatItemsz#SimpleX: error processing chat itemnewChatItemrcvFileCompletechatItemfile
fileSourcefilePathz/SimpleX: error processing deferred file messagez!SimpleX: unhandled event type: %s)
isinstancer\   dictrk   popr   
set_resultr$   
startswith_CORR_PREFIXrh   discardrR   rq   rr   r'   _send_commandr   _send_fire_and_forgetr   _handle_chat_itemr   r   rj   
setdefault)rs   r   r   corr_idr   	resp_typecontact_reqcontact_req_idrcv_filefile_id
chat_itemsitem	chat_itemchat_item_data	file_infopendingfile_source	file_pathpending_item_datas                      r   r   zSimplexAdapter._handle_eventr  sw      %/uyy/@/@$$G$GRuyy   U))H%%  	w$"999)--g66C88:: %t$$$F  	z'3// 	G4F4F|4T4T 	"**7333FHHV$$=		&"(=(=	 (((T-=(((#3R88>BK(__-?@@N)@s>2233   (()DN)D)DEEEEEEEEEF +++xx 1266<"H0:8T0J0JThll8,,,PTG"T   001Gg1G1GHHHHHHHHHF &&+r228bJj$// *(\
" L LL006666666666  L L L$$%JKKKKKLF %%H,,T2222222222 H H H  !FGGGGGHF )))R006BI&]]:r::@bN&**6266<"I1;It1L1LVimmH---RVG"w$2N'N'N6::7CC'mmL"==C "+t44KOOJ/// 
  (/J(C(C(Ir%"IN%00<<\J +<GJ'"44W==========$   ((M     F 	ILL<iHHHHH	I 	Is6   2J$J54J5K $LL(Q $Q+*Q+r   c           	        K   |                     di           pi }|                     di           pi }|                     dd          }|                     di           pi }|                     di           pi }|                     di           pi }|                     di           pi }t          |t                    r|                     dd          nd}	|	d	v rd
S t          |t                    r|                     dd          nd}
|
dk    rd
S d}t          |t                    r|                     dd          nd}|dv r|                     dd          }|s|dvrd
S d}d}d}d}|dk    r|                     di           pi }t          |                     dd                    }|                     dd          p)|                     di                                dd          }|}nA|dk    r|                     di           pi }t          |                     dd                    }d| }d}|                     di           pi }t          |                     dd                    }|                     dd          p)|                     di                                dd          }| j        r=d| j        vr3|| j        vr*t
                              dt          |                     d
S n9t
                              d            d
S t
                              d!|           d
S |st
                              d"           d
S g }g }|                     d#          }|rNt          |t                    r8|                     d$i           pi }t          |t                    r|                     d%          nd
}|                     d&d          }|                     d'          }d}|r&t          |          j	        
                                }|s(|r&t          |          j	        
                                }|sVt          |          rG|Et
                              d(|           || j        |<   |                     d)|            d
{V  d
S |rt          |          j	        
                                p)|r&t          |          j	        
                                nd}t          |          rA|                    |           |                    d*|                    d+                      nzt          |          rA|                    |           |                    d,|                    d+                      n*|                    |           |                    d-           |}|rX|                     di           pi }|                     dd          p)|                     d.i                                d|          }|                     |||rdnd/||p|0          }t&          j        }|rXt+          d1 |D                       rt&          j        }n2t+          d2 |D                       rt&          j        }nt&          j        }|                     d3          p|                     d4d          } 	 | r)t3          j        |                     d5d6                    }!nt3          j        t:          j        7          }!n6# t>          t@          f$ r" t3          j        t:          j        7          }!Y nw xY wtC          ||pd||||!|8          }"t
                              d9t          |          |d
d:         |pdd
d;                    |t&          j        k    r|r| "                    |"           d
S | #                    |"           d
{V  d
S )<z5Process a single chat item from a newChatItems event.chatInfor   r   rN   metacontent
msgContentchatDir)	directSndgroupSndNrcvMsgContent)textr   imagevoicelinkvideor   )r   r   r   Fdirectcontact	contactIdlocalDisplayNameprofiledisplayNamegroup	groupInfogroupIdgroup:TgroupMembermemberIdmemberProfile*z"SimpleX: group %s not in allowlistz:SimpleX: ignoring group message (no SIMPLEX_GROUP_ALLOWED)z SimpleX: unhandled chat type: %sz(SimpleX: ignoring message with no senderr   r   r   fileNamer   z;SimpleX: voice file %d not yet received, accepting transferr   image/.audio/zapplication/octet-streamgroupProfiledm)chat_id	chat_name	chat_typeuser_id	user_namec              3   @   K   | ]}|                     d           V  dS )r   Nr   r   mts     r   	<genexpr>z3SimplexAdapter._handle_chat_item.<locals>.<genexpr>p  s.      AAr2==**AAAAAAr   c              3   @   K   | ]}|                     d           V  dS )r   Nr  r  s     r   r	  z3SimplexAdapter._handle_chat_item.<locals>.<genexpr>r  s.      CCR]]8,,CCCCCCr   itemTs	createdAtZz+00:00)tz)sourcer   message_type
media_urlsmedia_types	timestampraw_messagez"SimpleX: message from %s in %s: %sr   2   )$r\   r   r   r$   rb   rq   r   r'   r   suffixr:   rB   rr   rj   r   r<   appendlstripbuild_sourcer   TEXTanyVOICEPHOTODOCUMENTr   fromisoformatreplacenowr   utc
ValueErrorAttributeErrorr   _enqueue_text_eventhandle_message)#rs   r   	chat_infor   r  r   r   msg_contentitem_directiondirection_typecontent_typer   msg_type_str	sender_idsender_namer   is_groupr   
group_infogroup_idmemberr  r  r   r   r   	file_namer   r6   r  r  msg_typets_strr  	msg_events#                                      r   r   z SimplexAdapter._handle_chat_item  s     MM*b117R	"z266<"MM&"--	!!&"--3 $$Y339rkk,339r (++Ir::@b.8.N.NVNvr***TV 	 666F 3=Wd2K2KSw{{62...QS?**F +5k4+H+HPKOOFB'''b 	 NNN??62..D 	,FFFF 	  mmIr228bGGKKR8899I!++&8"== %2B Bc-$$   GG'!!"{B77=2J:>>)R8899H)x))GH#''r::@bFFJJz26677I **%7<< %

A Ac-$$ 
 $ t444 (===LL<"8,,   FP   LL;YGGGF 	LLCDDDF !#
!#"&&v..	 )	CIt44 )	C#--b99?RK k400
+++ 
 "j"55ImmH--GC 59oo,2244 59 59oo,2244  	s!3!3 	8KQ   9B,W5 001Gg1G1GHHHHHHHHH C9oo,2244 6?GDOO*00222R  !%% C%%i000&&'A

3'A'ABBBB"3'' C%%i000&&'A

3'A'ABBBB%%i000&&'ABBB  	 	*"{B77=2J"'92>> **..C Cc-))  ""!)3ggt!.Y # 
 
 # 		0AA[AAAAA 0&,CC{CCCCC 0&,
 '/ (##@txxR'@'@	6 :$26>>#x3P3PQQ		$LHL999	N+ 	6 	6 	6 555III	6 !!!#!
 
 
	 	0y!!CRCLZR"		
 	
 	
 {'''D'$$Y/////%%i00000000000s   A
\ 0]]c                 B    |j         j        j         d|j         j         S )z-Session-scoped key for text message batching.:)r  rH   r   r   )rs   r   s     r   _text_batch_keyzSimplexAdapter._text_batch_key  s#    ,'-FF0DFFFr   c                 D   |                      |          }| j                            |          }||| j        |<   np|j        r$|j        r|j         d|j         n|j        |_        |j        r>|j                            |j                   |j                            |j                   | j                            |          }|r(|                                s|	                                 t          j        |                     |                    | j        |<   dS )z.Buffer a text event and reset the flush timer.N
)r9  ro   r\   r   r  extendr  rp   r   r   r   r   _flush_text_batch)rs   r   keyexisting
prior_tasks        r   r%  z"SimplexAdapter._enqueue_text_event  s(   ""5))-11#66.3D&s++z 8@Ux}44
4445:   ?#**5+;<<<$++E,=>>>377<<
 	 joo// 	 .5.A""3''/
 /
&s+++r   r>  c                   K   t          j                    }	 t          j        | j                   d{V  | j                            |d          }|s<	 | j                            |          |u r| j                            |d           dS dS t          	                    d|t          |j        pd                     |                     |           d{V  | j                            |          |u r| j                            |d           dS dS # | j                            |          |u r| j                            |d           w w xY w)z<Wait for the quiet period then dispatch the aggregated text.Nz+[SimpleX] Flushing text batch %s (%d chars)rN   )r   current_taskr   rn   ro   r   rp   r\   rq   rr   r%   r   r&  )rs   r>  rB  r   s       r   r=  z SimplexAdapter._flush_text_batch  s     +--	>- 6777777777.223==E  -11#66,FF.223===== GF KK=EJ$"%%  
 %%e,,,,,,,,,-11#66,FF.223===== GFt-11#66,FF.223==== Gs   <D AD :Ec                    | xj         dz  c_         t           | j          dt          t          j                    dz             }| j                            |           t          | j                  | j        k    rYt          | j                  | j        z
  }t          |          D ]-}	 | j        	                                 # t          $ r Y  nw xY w|S )u  Mint a new correlation ID and remember it for echo-filtering.

        We add every minted id to ``_pending_corr_ids`` so the inbound
        event loop can drop the daemon's echo of our own commands without
        ever invoking ``_handle_chat_item``. The set is bounded — when
        it grows past ``_max_pending_corr``, the oldest entries are
        evicted in a single sweep.
        r2   -  )rl   r   intr   rh   addr%   ri   ranger   KeyError)rs   r   overflow_s       r   _make_corr_idzSimplexAdapter._make_corr_id  s     	a!Q4#5QQDIKK$<N8O8OQQ""7+++t%&&)???4122T5KKH8__  *..0000   EEs   *C
CCpayloadc                   K   | j         }|st                              d           dS 	 |                    t	          j        |                     d{V  dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)u   Fire-and-forget JSON payload write.

        Drops cleanly when the WebSocket is missing or already closed; the
        caller never has to handle reconnection — the ``_ws_listener``
        loop does that out of band.
        z(SimpleX: WS send dropped (not connected)NzSimpleX: WS send error: %s)rc   rq   r   sendr   dumpsr   r   )rs   rM  r   r   s       r   _send_wszSimplexAdapter._send_ws  s       X 	LLCDDDF	<''$*W--........... 	< 	< 	<NN7;;;;;;;;;	<s   -A 
B"BBr   commandtimeoutc                   K   | j         }|st                              d           dS |                                 }t	          j        ||d          }t          j                    }|                                }|| j	        |<   	 |
                    |           d{V  t          j        ||           d{V }|S # t          j        $ rB t                              d|dd                    | j	                            |d           Y dS t          $ rJ}	t                              d|dd         |	           | j	                            |d           Y d}	~	dS d}	~	ww xY w)z1Send a command and await the correlated response.z1SimpleX: command sent but WebSocket not connectedNr   cmd)rS  zSimpleX: command timed out: %sr  u"   SimpleX: command failed: %s — %s)rc   rq   r   rL  r   rP  r   get_event_loopcreate_futurerk   rO  wait_forTimeoutErrorr   r   )
rs   rR  rS  r   r   rM  loopr   resultr   s
             r   r   zSimplexAdapter._send_command  s      X 	NNNOOO4$$&&*@@AA%''"0022+.(	'''""""""""""+CAAAAAAAAAFM# 	 	 	NN;WSbS\JJJ#''66644 	 	 	NN?"qQQQ#''66644444	s   8B> >AE!	E!?EE!c                 n   K   |                                  }|                     ||d           d{V  dS )a  Send a command without waiting for a correlated response.

        Use this for commands the daemon never sends a corrId reply for,
        such as ``/freceive``. Awaiting a corr-id reply on those would
        stall the event loop for the full command timeout.
        rU  N)rL  rQ  )rs   rR  r   s      r   r   z$SimplexAdapter._send_fire_and_forget  sK       $$&&mmww??@@@@@@@@@@@r   r   r   reply_tometadatac                   K   h d}t          j        d|          }|r(t          j        dd|                                          }|rz|                                 }|                    d          r+t          j        dd|dig          }d	|d
d          d| }	nd| d| }	|                     ||	d           d{V  |D ]}
t          j
                            |
          d                                         |v }|r|                     ||
           d{V }n|                     ||
           d{V }|j        s|c S t!          d          S )aM  Send a text message.

        If *content* contains ``MEDIA:<path>`` tags (embedded by TTS / audio
        tools to signal file attachments), they are stripped from the text
        body and sent as native voice notes or documents.

        Groups use the structured ``/_send #<id> json [...]`` form
        because the bracket chat-command syntax (``#[<id>] text``) is
        parsed by the daemon as a display-name lookup, which silently
        drops when the group's display name isn't the literal ID. DMs
        use the simple ``@<id> text`` form which has always worked in
        production.

        The call is fire-and-forget at the WebSocket level: the daemon
        doesn't always return a corrId reply for chat commands, and
        waiting for one would serialise all outbound traffic behind a
        30-second timeout.
        >   r?   r4   r0   r@   rA   zMEDIA:(\S+)z	MEDIA:\S+rN   r   r   r   r   r   /_send #   N json @ rU  r2   Tsuccess)refindallsubr   rL  r   r   rP  rQ  r^   pathsplitextr:   
send_voicesend_documentrh  r   )rs   r   r   r^  r_  _voice_extsmedia_pathsr   composedcmd_strrl  is_voicemedia_results                r   rO  zSimplexAdapter.send!  s     2 @??j99 	@f\2w77==??G 	E((**G!!(++ 2  :"VW$E$EFG  CWQRR[BBBB1g1111--77 C CDDDDDDDDD 	$ 	$Dw''--a06688KGH G%)__Wd%C%CCCCCCC%)%7%7%F%FFFFFFF' $####$ $''''r   r   c           
      .   ddl }ddl}t          |           }| }d}	 ddlm} |                    |           }|j                                        dvr8t          |	                    d                    }|
                    |d           |                                }|                    d           ddl}	|	                                }
|
                    |
d	d
           dt          j        |
                                                                          z   }nr# t&          $ rd 	 |j                                        dvr>t          |	                    d                    }|                    d| |gddd           |                    dd          5 }|j        }ddd           n# 1 swxY w Y   |                    d| dddd|gddd           t          |d          5 }dt          j        |                                                                          z   }ddd           n# 1 swxY w Y   t1          j        |           n9# t4          |j        f$ r%}t8                              d|           Y d}~nd}~ww xY wY nw xY w||fS )a  Ensure *file_path* is a PNG and return ``(png_path, thumb_data_uri)``.

        SimpleX clients can't display WebP and a few other formats inline.
        This converts to PNG when needed and generates a small JPEG thumbnail
        for the ``image`` field in the ``/_send`` payload so the chat shows
        an inline preview. Uses Pillow when available, falls back to
        ImageMagick ``convert``.
        r   NrN   )Image)r*   r+   r8   r*   PNG)   ry  JPEGF   )qualityzdata:image/jpg;base64,convertT   )checkcapture_outputrS  r+   F)r  deletez-resize128x128z-quality70rbz)SimpleX: image conversion unavailable: %s)
subprocesstempfiler   PILrw  openr  r:   r$   with_suffixsavecopy	thumbnailioBytesIObase64	b64encodegetvaluedecoder}   runNamedTemporaryFilenamereadr^   removeFileNotFoundErrorSubprocessErrorrq   r   )r   r  r  ppng_path	thumb_urirw  imgthumbr  buftmptmp_pathfexcs                  r   _prepare_imagezSimplexAdapter._prepare_image\  sY    	OO	1	Q!!!!!!**Y''Cx~~'@@@q}}V44555)))HHJJEOOJ'''III**,,CJJsFBJ///("3<<>>2299;;< I   	Q  	Q  	QQ8>>##+DDD"1==#8#899HNN"Ix8"'+ "	 #    00u0MM (QT"xH( ( ( ( ( ( ( ( ( ( ( ( ( ( (!!!!"  #'     (D)) Q063CAFFHH3M3M3T3T3V3VV                	(####%z'AB Q Q QJCPPPPPPPPQ? 	QD ""s   DD! !J-A0IF1%I1F5	5I8F5	93I,<H4(I4H8	8I;H8	<IJJ
%J JJ

JJ	image_urlcaptionc                   K   ddl m} |                    d          r ||dd                   }ni	 ddlm}  ||           d{V }nP# t
          $ rC}t                              d|           t          dt          |          	          cY d}~S d}~ww xY w|r!t          |                                          st          dd
	          S |                     |          \  }	}
t          j        |	d|
|pdddg          }|                    d          r|dd         }d| d| }nd| d| }|                     |           d{V }|t          d          S t          dd	          S )zASend an image. Supports ``file://`` URLs and ``http(s)://`` URLs.r   )unquotefile://   N)cache_image_from_urlz%SimpleX: failed to download image: %sFrh  r~   zImage file not foundr   rN   )r   r   r   r   r   r   rc  rb  rd  /_send @Trg  zFailed to send image)urllib.parser  r   gateway.platforms.baser  r   rq   r   r   r$   r   existsr  r   rP  r   )rs   r   r  r  rt   r  r   r  r   r  r  rr  r1  rR  r\  s                  r   
send_imagezSimplexAdapter.send_image  s      	)(((((	** 		?	!""..II?GGGGGG"6"6y"A"AAAAAAA		 ? ? ?FJJJ!%s1vv>>>>>>>>>?  	KY 6 6 8 8 	Ke3IJJJJ"11)<<) : !) '!* '2# # 	
 
 h'' 	;qrr{H;;;;;GG:::::G))'22222222d++++%/EFFFFs   A 
B8BBB
image_pathc                 :   K    | j         |d| fd|i| d{V S )z$Send a local image file via SimpleX.r  r  N)r  )rs   r   r  r  r^  rt   s         r   send_image_filezSimplexAdapter.send_image_file  s_       %T_+z++
 
5<
@F
 
 
 
 
 
 
 
 	
r   
video_pathc                 B   K   |                      |||           d{V S )z5Send a video file via SimpleX (as a file attachment).)r  N)ro  )rs   r   r  r  r^  rt   s         r   
send_videozSimplexAdapter.send_video  s5       ''W'MMMMMMMMMr   filenamec                   K   t          |                                          st          dd          S t          j        |d|pdddg          }|                    d          r|d	d
         }d| d| }nd| d| }|                     |           d
{V }	|	t          d          S t          dd          S )z Send a document/file attachment.FzFile not foundr  r   rN   ra  r  r   rc  Nrb  rd  r  Trg  zFailed to send documentr   r  r   r   rP  r   r   )
rs   r   r   r  r  rt   rr  r1  rR  r\  s
             r   ro  zSimplexAdapter.send_document  s      I%%'' 	Ee3CDDDD: !*+17=b"I"I 
 
 h'' 	;qrr{H;;;;;GG:::::G))'22222222d++++%/HIIIIr   r   
audio_pathdurationc                   K   t          |                                          st          dd          S t          j        d|pd|dd|idg          }|                    d	          r|d
d         }d| d| }	nd| d| }	|                     |	           d{V }
|
t          d          S t          dd          S )ac  Send an audio file as a SimpleX voice note (plays inline).

        SimpleX distinguishes a generic file attachment (``type: "file"``)
        from an inline voice note (``type: "voice"``). ``/f`` would deliver
        a downloadable file; the structured ``/_send`` form with
        ``msgContent.type == "voice"`` produces the voice-note player.
        FzVoice file not foundr  r   rN   )r   r   r  r   )r   r   r   rc  Nrb  rd  r  Trg  zFailed to send voice messager  )rs   r   r  r  r^  r  rt   rr  r1  rR  r\  s              r   rn  zSimplexAdapter.send_voice  s       J&&(( 	Ke3IJJJJ: !( '2$,# #
 $.z": 	
 
 h'' 	;qrr{H;;;;;GG:::::G))'22222222d++++%/MNNNNr   c                 
   K   dS )u.   SimpleX has no typing-indicator API — no-op.Nr   )rs   r   r_  s      r   send_typingzSimplexAdapter.send_typing9  s
        r   c                 X   K   |                     d          r|d|dd         dS |d|dS )zReturn basic chat info.r   r   rc  N)r   r   r  r   r  )rs   r   s     r   get_chat_infozSimplexAdapter.get_chat_info<  sE      h'' 	N&MMM"D'BBBr   r   N)r   )NNN)NNr   )*__name__
__module____qualname____doc__MAX_MESSAGE_LENGTHr   rZ   r`   r   r   r   r   r   r   r   r   r$   r9  r%  r=  rL  rQ  rm   r	   r   r   r   r   r   rO  staticmethodtupler  r  r  r  ro  rF  rn  r  r  __classcell__)rw   s   @r   rD   rD      s         ,;
~ ;
 ;
 ;
 ;
 ;
 ;
B t        D(- (- (- (-\5? 5? 5? 5?vP P P P(dI dI$ dI dI dI dILC1 C1$ C1 C1 C1 C1RG\ Gc G G G G
 
$ 
 
 
 
,>3 >4 > > > >,s    *<d <t < < < <" .2 %*	$   :A3 A4 A A A A$ #'-15( 5(5( 5( 3-	5(
 4S>*5( 
5( 5( 5( 5(v C## C#%S/ C# C# C# \C#R "&	2G 2G2G 2G #	2G 
2G 2G 2G 2Gp "&"&
 

 
 #	

 3-
 

 
 
 
" "&"&	N 	N	N 	N #		N
 3-	N 
	N 	N 	N 	N "&"&J JJ J #	J
 3-J 
J J J JH "&"&)O )O)O )O #	)O
 3-)O )O 
)O )O )O )OV= = = = = = =C3 C4S> C C C C C C C Cr   rD   c                  `    t          j        d          sdS 	 ddl} n# t          $ r Y dS w xY wdS )a  Plugin gate: require SIMPLEX_WS_URL AND the websockets package.

    Returning False keeps the platform out of ``get_connected_platforms()``
    so the gateway never instantiates the adapter when the dependency is
    missing or no daemon URL is configured.
    SIMPLEX_WS_URLFr   NT)r^   r_   r|   r}   )r|   s    r   check_requirementsr  G  sW     9%&& u   uu4s    
++c                     t          | di           pi }t          j        d          p|                    dd          }t	          |          S )z=Validate that the platform config has enough info to connect.rI   r  rJ   rN   r[   r^   r_   r\   r`   rE   rI   rJ   s      r   validate_configr  W  G    FGR((.BEY'((CEIIh,C,CF<<r   c                     t          | di           pi }t          j        d          p|                    dd          }t	          |          S )z9Check whether SimpleX is configured (env or config.yaml).rI   r  rJ   rN   r  r  s      r   is_connectedr  ^  r  r   c                     t          j        dd                                          } | sdS d| i}t          j        dd                                                                          }|r|dv|d<   t          j        dd                                          }|r||d	<   t          j        d
d                                          }|r/|t          j        dd                                          p|d|d<   |S )u,  Seed ``PlatformConfig.extra`` from env vars during gateway config load.

    Called by the platform registry's env-enablement hook BEFORE adapter
    construction, so ``gateway status`` and ``get_connected_platforms()``
    reflect env-only configuration without instantiating the WebSocket
    client. Returns ``None`` when SimpleX isn't minimally configured.

    The special ``home_channel`` key is handled by the core hook — it
    becomes a proper ``HomeChannel`` dataclass on the ``PlatformConfig``
    rather than being merged into ``extra``.
    r  rN   NrJ   rM   >   rO   rP   rQ   rR   rS   rT   SIMPLEX_HOME_CHANNELSIMPLEX_HOME_CHANNEL_NAME)r   r  home_channel)r^   r_   r   r:   )rJ   seedrR   rT   homes        r   _env_enablementr  e  s    Y',,2244F tF#D)1266<<>>DDFFK F)1EE]I5r::@@BBM . -_9+R006688D 
I92>>DDFFN$ 
  
^ Kr   F)	thread_idmedia_filesforce_documentr   messager  r  r  c                  K   	 ddl }n# t          $ r ddicY S w xY wt          | di           pi }t          j        d          p|                    dd          }|sdd	iS 	 |                    d
          r-|dd         }	t          j        dd|dig          }
d|	 d|
 }nd| d| }t           dt          t          j                    dz             |d}|                    |dd          4 d{V 	 }|                    t          j        |                     d{V  t          j        d           d{V  ddd          d{V  n# 1 d{V swxY w Y   dd|dS # t           $ r}dd| icY d}~S d}~ww xY w)u  Open an ephemeral WebSocket to the daemon, send, and close.

    Used by ``tools/send_message_tool._send_via_adapter`` when the gateway
    runner is not in this process (e.g. ``hermes cron`` running as a
    separate process from ``hermes gateway``). Without this hook,
    ``deliver=simplex`` cron jobs fail with "No live adapter for platform".

    ``thread_id`` and ``force_document`` are accepted for signature parity
    with other plugins but are not meaningful here. ``media_files`` is
    accepted but only the text body is delivered — SimpleX file transfers
    require the daemon's filesystem-backed flow, which an ephemeral
    connection cannot drive safely.
    r   Nr~   z5websockets not installed. Run: pip install websocketsrI   r  rJ   rK   z3SimpleX standalone send: SIMPLEX_WS_URL is requiredr   rc  r   r   ra  rb  rd  re  rf  zsnd-rE  rU  ry      )rz   r   g      ?TrG   )rh  rH   r   zSimpleX send failed: )r|   r}   r[   r^   r_   r\   r   r   rP  r   rF  r   r   rO  r   r   r   )pconfigr   r  r  r  r  r   rI   rJ   r1  rr  rs  rM  r   r   s                  r   _standalone_sendr    s     ,R&&&&& R R RPQQQQR GWb))/REY'(( EII'- -F  PNOO6h'' 	.qrr{Hz A ABC H <;;;;GG .'--G--G &DD3ty{{T/A+B+BDD
 

 $$1 % 
 
 	% 	% 	% 	% 	% 	% 	% 	%''$*W--.........-$$$$$$$$$	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	% 	%  Y7KKK 6 6 644455555556sP   	 !BE+ :AEE+ 
EE+ !E"E+ +
F5F;FFc            	         t                       t          d           t          d           t          d           t          d           t          d           t                       	 ddlmm n # t          $ r t          d           Y d	S w xY wd
ddt
          dt
          dt          dd	ffd}  | dd            | dd            | dd            | dd            | dd           t          d           d	S )u   Minimal stdin wizard for ``hermes setup gateway`` → SimpleX.

    Prompts for the WebSocket URL and the optional allowlist / groups /
    auto-accept / home channel. Writes to ``~/.hermes/.env`` via
    ``hermes_cli.config``.
    zSimpleX Chat setupz------------------zRequirements:z?  1. simplex-chat daemon running (e.g. `simplex-chat -p 5225`).zF  2. Python package `websockets` installed (`pip install websockets`).r   )get_env_valuesave_env_valuezNhermes_cli.config not available; set SIMPLEX_* vars manually in ~/.hermes/.envNF)secretvarpromptr  r   c                :   t                    r |           nd }|rdnd}	 |rddlm}  || | d          }n&t          | | d                                          }n&# t
          t          f$ r t                       Y d S w xY w|r | |           d S d S )Nz [keep current]rN   r   )masked_secret_promptz: )callablehermes_cli.secret_promptr  inputr   EOFErrorKeyboardInterruptprint)	r  r  r  r?  r  r  r   r  r  s	          r   _promptz"interactive_setup.<locals>._prompt  s    )1-)@)@J==%%%d&.6""B	 =IIIIII,,-B-B-B-BCC333344::<<+, 	 	 	GGGFF	  	'N3&&&&&	' 	's   ?A% %BBr  z2Daemon WebSocket URL (default ws://127.0.0.1:5225)SIMPLEX_ALLOWED_USERSzAAllowed contactIds or display names (comma-separated; blank=skip)rS   zIAllowed group IDs (comma-separated, or '*' for any; blank=disable groups)rM   zAAuto-accept incoming contact requests? (true/false, default true)r  z(Home channel contact/group ID (or empty)zODone. Make sure the simplex-chat daemon is running before starting the gateway.)r  hermes_cli.configr  r  r}   r$   r`   )r  r  r  s    @@r   interactive_setupr    s    
GGG	
	
	/	
KLLL	
RSSS	GGGCCCCCCCCC   	
 	
 	
 	 :? ' ' 'S '# '$ '4 ' ' ' ' ' ' ' GRSSSG#%hiiiGS   GK   G"$NOOO		    s   +A4 4BBc                     |                      ddd t          t          t          dgdt          t
          dt          ddt          d	d
d
d           dS )uE   Plugin entry point — called by the Hermes plugin system at startup.rG   zSimpleX Chatc                      t          |           S r  )rD   )cfgs    r   <lambda>zregister.<locals>.<lambda>  s    N3$7$7 r   r  zJpip install websockets   # SimpleX adapter requires the websockets packager  r  SIMPLEX_ALLOW_ALL_USERSu   🔒Ta  You are chatting via SimpleX Chat, a private decentralised messenger. Contacts are identified by opaque internal IDs, not phone numbers or usernames. SimpleX supports standard markdown formatting. There is no typing indicator and no hard message length limit, but keep responses conversational. You can attach native images, voice notes, and arbitrary files; the adapter handles MEDIA:<path> tags by sending them as inline voice notes (audio extensions) or documents.)r  labeladapter_factorycheck_fnr  r  required_envinstall_hintsetup_fnenv_enablement_fncron_deliver_env_varstandalone_sender_fnallowed_users_envallow_all_envmax_message_lengthemojipii_safeallow_update_commandplatform_hintN)register_platformr  r  r  r  r  r  r  )ctxs    r   registerr    sq    77#'!&'! #)3-1/- !E3  " " " " "r   r  )4r  r   r  r   loggingr^   r   ri  r   r   r   pathlibr   typingr   r   r   r	   gateway.configr
   r   r  r   r   r   r   	getLoggerr  rq   r  r   r   r   r   r   r$   r   r'   bytesr5   r`   r<   rB   rD   r  r  r  r   r  r  r  r  r   r   r   <module>r     sL  , ,\     				  				  ' ' ' ' ' ' ' '       , , , , , , , , , , , ,
 4 3 3 3 3 3 3 3            
	8	$	$
     $  >S >T#Y > > > >
!3 !3 ! ! ! !5 S    *Es Et E E E ELs Lt L L L LwC wC wC wC wC( wC wC wC|D     t    D    $    N  $'+ ;6 ;6 ;6;6 ;6
 };6 $s)$;6 ;6 
#s(^;6 ;6 ;6 ;6|5 5 5 5p$ $ $ $ $ $r   