
    |+j                    ,   d Z ddlm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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mZ erddlZdZn	 ddlZdZn# e$ r dZdZY nw xY wdd	lmZmZ dd
lm Z m!Z!m"Z"m#Z# ddl$m%Z% ddl&m'Z'  ej(        e)          Z*dZ+dZ,dZ-dZ.dZ/ ee0          j1        dz  Z2ddgZ3dFdZ4dGdZ5dHd Z6dHd!Z7dId#Z8 G d$ d%e           Z9dJd)Z:d*d+d,d-d*d*d*d.Z;d/d/d0d1d/d2d2d3Z<dd4dKd:Z=dddd;dLdCZ>dMdEZ?dS )Nu2  
Photon Spectrum (iMessage) platform adapter for Hermes Agent.

Both directions of traffic flow through a small supervised Node sidecar
(see ``sidecar/index.mjs``) that runs the ``spectrum-ts`` SDK — the SDK is
TypeScript-only and there is no public HTTP message API, so a sidecar is
unavoidable.

Inbound:
    The SDK's ``app.messages`` is a long-lived **gRPC** stream. The sidecar
    serializes each message to a normalized JSON event and streams it to this
    adapter over a loopback ``GET /inbound`` (NDJSON). A background task here
    consumes that stream, dedupes on ``messageId``, and dispatches a
    ``MessageEvent`` to the gateway via ``BasePlatformAdapter.handle_message``.
    No webhook, no public URL, no signing secret.

Outbound:
    ``send`` / ``send_typing`` are loopback POSTs to the sidecar's control
    endpoints, authenticated with a shared bearer token.  Outbound media
    (images, voice notes, video, documents) goes through spectrum-ts'
    ``attachment()`` / ``voice()`` content builders via the sidecar's
    ``/send-attachment`` endpoint.
    )annotationsN)datetimetimezone)Path)TYPE_CHECKINGAnyDictListOptionalTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResultstrip_markdown   )load_project_credentialsiU"  z	127.0.0.1i@  i  i  sidecarz#(?<![\w@])@?hermes\s+agent\b[,:\-]?z(?<![\w@])@?hermes\b[,:\-]?valuer   defaultintreturnc                T    	 t          |           S # t          t          f$ r |cY S w xY wN)r   	TypeError
ValueError)r   r   s     ?/usr/local/lib/hermes-agent/plugins/platforms/photon/adapter.py_coerce_portr    d   s<    5zzz"   s    ''boolc                     t           sdS t          j        t          j        d          pd          sdS t
          dz                                  sdS dS )zEReturn True when both Python deps and the Node sidecar are available.FPHOTON_NODE_BINnodenode_modulesT)HTTPX_AVAILABLEshutilwhichosgetenv_SIDECAR_DIRexists     r   check_requirementsr/   k   s[     u<	"344>?? u>)1133  u4r.   cfgr   c                   | j         pi }|                    d          pt          j        d          }|                    d          pt          j        d          }|r|s"t	                      \  }}t          |o|          S dS )N
project_idPHOTON_PROJECT_IDproject_secretPHOTON_PROJECT_SECRETT)extragetr)   r*   r   r!   )r0   r6   r2   r4   	stored_id
stored_secs         r   validate_configr:   y   s    IOE<((JBI6I,J,JJYY/00VBI>U4V4VN .^ . 8 : :	:I,*---4r.   c                     t          |           S r   )r:   r0   s    r   is_connectedr=      s    3r.   Optional[dict]c                     t                      \  } }| r|sdS | |d}t          j        dd                                          }|r|t          j        dd          d|d<   |S )	zSeed PlatformConfig.extra from env so env-only setups appear in status.

    The special ``home_channel`` key is handled by the core plugin hook and
    becomes a proper ``HomeChannel`` on ``PlatformConfig``.
    N)r2   r4   PHOTON_HOME_CHANNEL PHOTON_HOME_CHANNEL_NAMEHome)chat_idnamehome_channel)r   r)   r*   strip)r2   r4   seedhomes       r   _env_enablementrJ      s     ":!;!;J > t$GGD9*B//5577D 
I8&AA 
  
^ Kr.   c                  d    e Zd ZdZeZdL fdZedMd	            ZdNdZ	dOdZ
dPdZdQdZdQdZdRdZdSdZdTdZdQdZdUdZdQdZ	 	 dVdWd'Z	 	 	 dXdY fd*Z	 	 	 dXdZd,Z	 	 	 dXd[d.Z	 	 	 dXd\d0Z	 	 	 	 d]d^d3Z	 	 	 dXd_d5Zd`dad6Zdad7Zdbd8Zdcd9Z	 	 	 	 ddded@ZdfdBZ ddddCdDdgdIZ!dhdKZ" xZ#S )iPhotonAdapterzBidirectional bridge to Photon Spectrum via the Node spectrum-ts sidecar.

    Inbound: consume the sidecar's ``/inbound`` gRPC stream.
    Outbound: loopback POSTs to the sidecar's control channel.
    configr   c                   t                                          |t          d                     |j        pi }t	                      \  }}t          j        d          p|                    d          p|pd| _        t          j        d          p|                    d          p|pd| _	        t          |                    d          pt          j        d          t                    | _        t          | _        t          j        d	          pt          j        d
          | _        t%          t          j        dd                                                    dv| _        t          j        d          pt+          j        d          pd| _        d | _        d | _        d | _        d| _        d | _        i | _        |                    d          }|t          j        d          }t%          |                                                                          dv | _        |                      d|v r|d         nt          j        d                    | _!        d S )Nphotonr3   r2   rA   r5   r4   sidecar_portPHOTON_SIDECAR_PORTPHOTON_SIDECAR_TOKEN   PHOTON_SIDECAR_AUTOSTARTtrue)0falsenor#   r$   Frequire_mentionPHOTON_REQUIRE_MENTION>   1onyesrU   mention_patternsPHOTON_MENTION_PATTERNS)"super__init__r   r6   r   r)   r*   r7   _project_id_project_secretr    _DEFAULT_SIDECAR_PORT_sidecar_port_DEFAULT_SIDECAR_BIND_sidecar_bindsecrets	token_hex_sidecar_tokenstrlower_autostart_sidecarr'   r(   	_node_bin_sidecar_proc_sidecar_supervisor_task_inbound_task_inbound_running_http_client_seen_messagesrG   rY   _compile_mention_patterns_mention_patterns)selfrM   r6   r8   r9   _require_mention	__class__s         r   ra   zPhotonAdapter.__init__   s]   (!3!3444"
 !9 : :	:I)** yy&& 	 	 I-.. yy)** 	 	 *IIn%%I3H)I)I!
 
 3I,--F1B21F1F 	 #&I0&99#
 #

%''-#. #455Wf9M9MWQW :>@D%59 %;? 13
 !99%677#!y)ABB"#344::<<BBDD I
  
 "&!?!?!U** $%%455"
 "
r.   rawr   r   'list[re.Pattern]'c                   | t          t                    }nt          | t                    rv|                                 }	 |rt          j        |          ng }n# t          $ r d}Y nw xY wt          |t                     r|nd |                                D             }nt          | t                     r| }n| g}g }|D ]}t          |                                          }|s&	 |	                    t          j        |t          j                             Z# t          j        $ r&}t                              d||           Y d}~d}~ww xY w|S )aD  Compile group-mention wake words from config/env.

        ``raw`` is a list (config or env JSON), a string (env var: JSON
        list, or comma/newline-separated), or None (use Hermes defaults).
        Mirrors the BlueBubbles implementation so both iMessage channels
        accept the same configuration shapes.
        Nc                f    g | ].}|                     d           D ]}|                                /S ),)splitrG   ).0lineparts      r   
<listcomp>z;PhotonAdapter._compile_mention_patterns.<locals>.<listcomp>   sZ     @ @ @ JJsOO@ @  

@ @ @ @r.   z'[photon] Invalid mention pattern %r: %s)list_DEFAULT_MENTION_PATTERNS
isinstancerk   rG   jsonloads	Exception
splitlinesappendrecompile
IGNORECASEerrorloggerwarning)rz   patternstextloadedcompiledpatternexcs          r   ru   z'PhotonAdapter._compile_mention_patterns   s    ;566HHS!! 	99;;D-19D)))r   !+FD!9!9 vv @ @ OO--@ @ @HH
 T"" 	HHuH') 	U 	UGw<<%%''D U
4 ? ?@@@@8 U U UH$PSTTTTTTTTUs*   A A*)A*)2DE+EEr   rk   r!   c                Z    r| j         sdS t          fd| j         D                       S )NFc              3  B   K   | ]}|                               V  d S r   )search)r   r   r   s     r   	<genexpr>zBPhotonAdapter._message_matches_mention_patterns.<locals>.<genexpr>  s/      NNG7>>$''NNNNNNr.   )rv   any)rw   r   s    `r   !_message_matches_mention_patternsz/PhotonAdapter._message_matches_mention_patterns  s@     	41 	5NNNNt7MNNNNNNr.   c                   |s|S | j         D ]r}|                    |                                          }|rG|                                |                                d                             d          }|p|c S s|S )zStrip a leading wake word before dispatch.

        Custom mention patterns are regexes, so we only strip a leading
        match to avoid deleting ordinary words later in the prompt.
        Nz ,:-)rv   matchlstripend)rw   r   r   r   cleaneds        r   _clean_mention_textz!PhotonAdapter._clean_mention_text  s      	K- 	' 	'GMM$++--00E '++--		5<<VDD$&&&' r.   c                  K   t           s|                     ddd           dS | j        r| j        s|                     ddd           dS t	          j        d          }|| _        | j        rp	 |                                  d {V  nn# t          $ rG}|                     d	d
| d           |
                                 d {V  d | _        Y d }~dS d }~ww xY wt                              d           d| _        t          j                                        |                                           | _        |                                  t                              d| j        | j                   dS )NMISSING_DEPhttpx not installedF)	retryableMISSING_CREDENTIALSzRPHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET are required. Run: hermes photon setup      >@timeoutSIDECAR_FAILEDz failed to start Photon sidecar: TuD   [photon] sidecar autostart disabled — inbound + outbound will failuD   [photon] connected — sidecar on %s:%d, streaming inbound over gRPC)r&   _set_fatal_errorrb   rc   httpxAsyncClientrs   rm   _start_sidecarr   acloser   r   rr   asyncioget_event_loopcreate_task_inbound_looprq   _mark_connectedinforg   re   )rw   clientes      r   connectzPhotonAdapter.connect!  s      	!!4 "    5 	t'; 	!!%+	 "    5"4000" " 	
))++++++++++   %%$:q::" &   
 mmoo%%%%%%%$(!uuuuu NNV  
 !%$355AA  
 
 	R 2	
 	
 	
 ts   0B 
C<CCNonec                  K   d| _         | j        O| j                                         	 | j         d {V  n # t          j        $ r Y nt
          $ r Y nw xY wd | _        |                                  d {V  | j        8	 | j                                         d {V  n# t
          $ r Y nw xY wd | _        | 	                                 d S )NF)
rr   rq   cancelr   CancelledErrorr   _stop_sidecarrs   r   _mark_disconnected)rw   s    r   
disconnectzPhotonAdapter.disconnectS  s-      %)%%'''((((((((()      !%D  """""""""('..0000000000    $D!!!!!s'   9 A
	AAB" "
B/.B/c                  K   | j         }|dS d| j         d| j         d}d| j        i}d}| j        rI	 |                    d||d          4 d{V 	 }|j        d	k    rt          d
|j                   d}|                                2 3 d{V }| j        s n4|	                                }|s'| 
                    |           d{V  C6 	 ddd          d{V  n# 1 d{V swxY w Y   n|# t          j        $ r  t          $ r`}| j        sY d}~dS t                              d||           t          j        |           d{V  t#          |dz  d          }Y d}~nd}~ww xY w| j        GdS dS )zConsume the sidecar's ``/inbound`` NDJSON stream, with reconnect.

        The sidecar owns the gRPC reconnect/heartbeat to Photon; this loop
        only has to re-open the loopback HTTP stream if it drops (e.g. the
        sidecar restarts).
        Nhttp://:z/inboundX-Hermes-Sidecar-Tokeng      ?GETheadersr      z/inbound returned z;[photon] inbound stream dropped (%s); reconnecting in %.1fs   r   )rs   rg   re   rj   rr   streamstatus_codeRuntimeErroraiter_linesrG   _on_inbound_liner   r   r   r   r   sleepmin)rw   r   urlr   backoffrespr   r   s           r   r   zPhotonAdapter._inbound_loopi  s      ">FI*IIT-?III+T-@A# 	11!==3 )   : : : : : : : :'3..*+R@P+R+RSSS!G&*&6&6&8&8 : : : : : : :d#4 "!E#zz||# %$"33D9999999999 '9&8: : : : : : : : : : : : : : : : : : : : : : : : : : : )    1 1 1, EEEEEQw   mG,,,,,,,,,gk4001# # 	1 	1 	1 	1 	1sN   C7 9C%C>C%C7 %
C//C7 2C/3C7 7E0E+A	E++E0r   c                  K   	 t          j        |          }n0# t           j        $ r t                              d           Y d S w xY w|                    d          }|r|                     |          rd S 	 |                     |           d {V  d S # t          $ r t          	                    d           Y d S w xY w)Nz'[photon] skipping non-JSON inbound line	messageIdz [photon] inbound dispatch failed)
r   r   JSONDecodeErrorr   debugr7   _is_duplicate_dispatch_inboundr   	exception)rw   r   eventmsg_ids       r   r   zPhotonAdapter._on_inbound_line  s      	Jt$$EE# 	 	 	LLBCCCFF	 ;'' 	d((00 	F	A((/////////// 	A 	A 	A?@@@@@@	As!    )AA8B $B=<B=r   c                X   t          j                     }| j        }|                    |          }|||z
  t          k     rdS ||v r||= |||<   t	          |          t
          k    rDt          |                                          d t	          |          t
          z
           D ]}||= dS )NTF)timert   r7   _DEDUP_WINDOW_SECONDSlen_DEDUP_MAX_SIZEr   keys)rw   r   nowseentolds         r   r   zPhotonAdapter._is_duplicate  s    ikk"HHV=S1W'<<<4 T>>VVt99&&DIIKK(()F3t99+F)FG  IIur.   r   Dict[str, Any]c           
       K   |                     d          pi }|                     d          pi }|                     d          pi }|                     d          pd}|st                              d           dS |                     d          d	k    rd	nd
}|                     d          p|                     d          p|}|                     d          pd}	 |r(t          j        |                    dd                    nt          j        t          j                  }	n/# t          $ r" t          j        t          j                  }	Y nw xY wg }
g }|                     d          }|dk    r%|                     d          pd}t          j        }n|dv r|dk    }|                     d          p|rdnd}|                     d          pd}|rt          j        nt          |          }t          ||||          }|r7|
                    |           |                    |p|rdnd           |rdnd}nb|rdnd}|                     d          }t!          |t"          t$          f          rd| dnd}d| d | d!|pd" | d#}nd$| d%}t          j        }|d	k    rM| j        rF|                     |          st                              d&           dS |                     |          }|                     |||||pd'          }t1          ||||                     d(          ||	|
|)          }|                     |           d{V  dS )*au  Normalize a sidecar inbound event and dispatch it to the gateway.

        Event shape (from ``sidecar/index.mjs``)::

            {
              "messageId": "...",
              "platform": "iMessage",
              "space": {"id": "...", "type": "dm"|"group", "phone": "+E164"},
              "sender": {"id": "+E164"},
              "content": {"type": "text", "text": "..."}
                       | {"type": "attachment"|"voice", "id", "name",
                          "mimeType", "size", "duration"?, "data"?,
                          "encoding"?},
              "timestamp": "2026-05-14T19:06:32.000Z"

        Attachment and voice content carry the bytes inline as base64 ``data``
        (with ``encoding == "base64"``) when the sidecar could read them
        within its size cap; otherwise only metadata is present and we surface
        a marker.
            }
        spacesendercontentidrA   z![photon] inbound missing space.idNtypegroupdmphone	timestampZz+00:00)tzr   >   voice
attachmentr   rE   z	(unnamed)mimeTypeforce_audio	audio/mp4zapplication/octet-streamz(voice)z(attachment)r   durationz, duration: sz[Photon z received: z (zunknown MIMEz)]z"[Photon content type not handled: ]zR[photon] ignoring group message (require_mention=true, no mention pattern matched))rD   	chat_name	chat_typeuser_id	user_namer   )r   message_typesource
message_idraw_messager   
media_urlsmedia_types)r7   r   r   r   fromisoformatreplacer   r   utcr   r   TEXTVOICE_attachment_message_type_cache_inbound_attachmentr   r   r   floatrY   r   r   r   build_sourcer   handle_message)rw   r   r   r   r   space_idr   	sender_idts_strr   r  r  ctyper   mtypeis_voicerE   mimecachedlabelr   duration_textr  message_events                           r   r   zPhotonAdapter._dispatch_inbound  s+     , 		'""(b8$$*))I&&,"99T??(b 	NN>???F  %yy00G;;GG	JJt$$F		'(:(:Fh	;''-2	6 3&v~~c8'D'DEEE\X\222 I
  	6 	6 	6 555III	6
 !#
!#F##F??;;v&&,"D$EE---'H;;v&&Ph+O77KD;;z**0bD)1UK%%7OPT7U7UE.t  F  !!&)))""UHT[[:T   %-@yy.
 $,=";;z22 "(S%L99.8.... Bu B B B B.B0=B B B 
 A@@@D$E D$899$?? I   ++D11D""'4 # 
 
 %yy--!#	
 	
 	
 !!-00000000000s   A	D( ()EEc                "  K   t           dz                                  st          dt            d          t          j                                        }| j        |d<   | j        |d<   t          | j	                  |d<   | j
        |d<   | j        |d<   t          j        | j        t          t           d	z            gt          j        t          j        |t"          j        d
k              | _        t)          j                    }|                    |                     | j                            | _        t3          j                    dz   }d }t5          j        d          4 d {V }t3          j                    |k     r| j                                        t          d| j        j         d          	 |                    d| j
         d| j	         dd| j        i           d {V }|j        dk    r	 d d d           d {V  d S n# t4          j         $ r}|}Y d }~nd }~ww xY wt)          j!        d           d {V  t3          j                    |k     	 d d d           d {V  n# 1 d {V swxY w Y   t          d|           )Nr%   z+Photon sidecar deps not installed. Run: cd z, && npm install   (or `hermes photon setup`)r3   r5   rQ   PHOTON_SIDECAR_BINDrR   z	index.mjswin32)stdoutstderrenvstart_new_sessiong      .@       @r   z Photon sidecar exited with code z before becoming readyr   r   z/healthzr   )r   r   g?z0Photon sidecar did not become ready within 15s: )"r+   r,   r   r)   environcopyrb   rc   rk   re   rg   rj   
subprocessPopenrn   PIPESTDOUTsysplatformro   r   r   r   _supervise_sidecarrp   r   r   r   poll
returncodepostr   RequestErrorr   )rw   r"  loopdeadlinelast_errr   r   r   s           r   r   zPhotonAdapter._start_sidecar,  s     ~-5577 	Q"Q Q Q   joo#'#3 '+';#$%();%<%<!"%)%7!"&*&9"#'-^S!;<<=?$"|w6
 
 
 %''(,(8(8##D$677)
 )
%
 9;;%(,$S111 	) 	) 	) 	) 	) 	) 	)V)++((%**,,8&Q-8Q Q Q  !!'S$"4SSt7ISSS!94;N O "- " "      D '3..	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) /) ! ! ! HHHHHH!mC((((((((( )++(((	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)" IxII
 
 	
s>   (AI.7A HI.H'H"I."H''4I..
I8;I8procsubprocess.Popenc                  K   |j         dS |j         }t          j                    }	 	 |                    d|j                   d{V }|sdS t
                              d|                    dd                                                     g# t          $ r&}t
          
                    d|           Y d}~dS d}~ww xY w)z1Pump the sidecar's stdout/stderr into our logger.NTz[photon-sidecar] %szutf-8r	  z&[photon-sidecar] supervisor exited: %s)r   r   r   run_in_executorreadliner   r   decoderstripr   r   )rw   r5  r   r2  r   r   s         r   r-  z PhotonAdapter._supervise_sidecar_  s      ;F%''	H]!11$HHHHHHHH E14;;w	3R3R3Y3Y3[3[\\\	]
  	H 	H 	HNNCQGGGGGGGGG	Hs   $B AB 
B?B::B?c                z  K   | j         }|d S 	 | j        M	 | j                            d| j         d| j         dd| j        id           d {V  n# t          $ r Y nw xY w	 |                    d           n# t          j	        $ r t          j        d	k    rc	 t          j        t          j        |j                  t           j                   n?# t$          t&          f$ r |                                 Y nw xY w|                                 	 |                    d           n)# t          j	        $ r |                                 Y nw xY wY nw xY wd | _         | j        "| j                                         d | _        d S d S # d | _         | j         | j                                         d | _        w xY w)
Nr   r   z	/shutdownr   r$  r   g      @r   r  )ro   rs   r0  rg   re   rj   r   waitr'  TimeoutExpiredr+  r,  r)   killpggetpgidpidsignalSIGTERMProcessLookupErrorPermissionError	terminatekillrp   r   )rw   r5  s     r   r   zPhotonAdapter._stop_sidecarn  s9     !<F	5 ,+00T$"4TTt7ITTT!94;N O # 1          
 !   D 		#	&&&&,      <7**)	"*TX"6"6GGGG.@ ) ) )((((() NN$$$ IIcI****!0      IIKKKKK   "&D,8-4466604--- 98 "&D,8-4466604-4444s   F
 ;A F
 
A F
 A  F
 $A; :F
 ;E6CE%C:7E9C::ED)(E)#EEEEF
 EF
 
0F:NrD   r   reply_toOptional[str]metadataOptional[Dict[str, Any]]r   c                d   K   |                      ||                     |                     d {V S r   )_sidecar_sendformat_message)rw   rD   r   rH  rJ  s        r   sendzPhotonAdapter.send  s>       ''1D1DW1M1MNNNNNNNNNr.   	image_urlcaptionc                   K   	 ddl m}  ||           d {V }n:# t          $ r- t                                          ||||           d {V cY S w xY w|                     |||           d {V S )Nr   )cache_image_from_urlrQ  )gateway.platforms.baserS  r   r`   
send_image_sidecar_send_attachment)	rw   rD   rP  rQ  rH  rJ  rS  
local_pathry   s	           r   rV  zPhotonAdapter.send_image  s      	SCCCCCC33I>>>>>>>>JJ 	S 	S 	S++GYRRRRRRRRRRR	S 22Z 3 
 
 
 
 
 
 
 
 	
s    4AA
image_pathc                B   K   |                      |||           d {V S NrT  rW  )rw   rD   rY  rQ  rH  rJ  kwargss          r   send_image_filezPhotonAdapter.send_image_file  J       22Z 3 
 
 
 
 
 
 
 
 	
r.   
audio_pathc                D   K   |                      |||d           d {V S )Nr   )rQ  kindr\  )rw   rD   r`  rQ  rH  rJ  r]  s          r   
send_voicezPhotonAdapter.send_voice  sL       22Zw 3 
 
 
 
 
 
 
 
 	
r.   
video_pathc                B   K   |                      |||           d {V S r[  r\  )rw   rD   rd  rQ  rH  rJ  r]  s          r   
send_videozPhotonAdapter.send_video  r_  r.   	file_path	file_namec                D   K   |                      ||||           d {V S )N)rE   rQ  r\  )rw   rD   rg  rQ  rh  rH  rJ  r]  s           r   send_documentzPhotonAdapter.send_document  sL       22YY 3 
 
 
 
 
 
 
 
 	
r.   animation_urlc                D   K   |                      |||||           d {V S r   )rV  )rw   rD   rk  rQ  rH  rJ  s         r   send_animationzPhotonAdapter.send_animation  sG       __]GXx
 
 
 
 
 
 
 
 	
r.   c                   K   	 |                      d|dd           d {V  d S # t          $ r&}t                              d|           Y d }~d S d }~ww xY w)N/typingstartspaceIdstatez[photon] send_typing failed: %s_sidecar_callr   r   r   )rw   rD   rJ  r   s       r   send_typingzPhotonAdapter.send_typing  s      	?$$wAA            	? 	? 	?LL:A>>>>>>>>>	?   % 
AAAc                   K   	 |                      d|dd           d {V  d S # t          $ r&}t                              d|           Y d }~d S d }~ww xY w)Nro  stoprq  z[photon] stop_typing failed: %srt  )rw   rD   r   s      r   stop_typingzPhotonAdapter.stop_typing  s      	?$$w@@            	? 	? 	?LL:A>>>>>>>>>	?rw  c                   K   |d|dS )zReturn whatever we know about a Spectrum space id.

        Photon's ``space.id`` is opaque; the inbound event also carries the
        DM/group type, but here we only have the id, so infer conservatively.
        r   )rE   r   r   r-   )rw   rD   s     r   get_chat_infozPhotonAdapter.get_chat_info	  s        W===r.   c                     t          |          S r   r   )rw   r   s     r   rN  zPhotonAdapter.format_message  s    g&&&r.   r   r$  max_retriesr   
base_delayr  c                p  K   |                      |          }|                     ||||           d{V }|j        r|S |j        pd}	|j        p|                     |	          }
|
s|                     |	          r|S |
rt          d|dz             D ]}|d|dz
  z  z  }t          	                    d||||	           t          j        |           d{V  |                     ||||           d{V }|j        r|c S |j        pd}	|j        s|                     |	          s nt                              d||	           |S t          	                    d|	           |                     ||d| j                 ||           d{V }|j        s t                              d	|j                   |S )
zIPhoton/iMessage is plain text, so never show the generic Markdown banner.)rD   r   rH  rJ  NrA   r   r   z;[photon] Send failed (attempt %d/%d, retrying in %.1fs): %sz8[photon] Failed to deliver response after %d retries: %sz6[photon] Send failed: %s - retrying plain-text messagez)[photon] Plain-text retry also failed: %s)rN  rO  successr   r   _is_retryable_error_is_timeout_errorranger   r   r   r   MAX_MESSAGE_LENGTH)rw   rD   r   rH  rJ  r~  r  r   result	error_str
is_networkattemptdelayfallback_results                 r   _send_with_retryzPhotonAdapter._send_with_retry  s}      ""7++yy	 ! 
 
 
 
 
 
 
 
 > 	ML&B	%L)A)A))L)L
 	d44Y?? 	M 	 K!O44  "aGaK&89Q[%   mE*********#yy# %%	  )           > "!MMM"L.B	( D,D,DY,O,O EN   D	
 	
 	
 !%		24223	 !* !
 !
 
 
 
 
 
 
 & 	]LLDoF[\\\r.   r  c                  K   t          |          | j        k    r=t                              dt          |          | j                   |d | j                 }||d}	 |                     d|           d {V }n5# t
          $ r(}t          dt          |                    cY d }~S d }~ww xY wt          d|                    d                    S )	Nz0[photon] truncating outbound from %d to %d charsrr  r   /sendFr  r   Tr   r  r  )	r   r  r   r   ru  r   r   rk   r7   )rw   r  r   bodydatar   s         r   rM  zPhotonAdapter._sidecar_sendU  s      t99t...NNBD		42   1$112D+3TBB	;++GT::::::::DD 	; 	; 	;e3q66:::::::::	;$488K3H3HIIIIs   A; ;
B-B("B-(B-r   )rE   	mime_typerQ  rb  pathrE   r  rb  c                 K   |                      t          |                    }|st          dd|           S |s ddl}|                    |          \  }	}
|	pd}|||dk    rdndd}|r||d	<   |r||d
<   |r||d<   	 |                     d|           d{V }n5# t          $ r(}t          dt          |                    cY d}~S d}~ww xY wt          d|                    d                    S )a  POST a local file to the sidecar's ``/send-attachment`` endpoint.

        ``kind`` is ``"voice"`` for audio sent as a voice note (downgrades
        to a plain audio attachment on platforms without voice notes),
        otherwise ``"attachment"``. spectrum-ts infers ``name`` and
        ``mimeType`` from the file extension; we only pass overrides when
        Hermes supplied them.
        Fz#unsafe or missing attachment path: r  r   Nr   r   rr  r  rb  rE   r   rQ  /send-attachmentTr   r  )validate_media_delivery_pathrk   r   	mimetypes
guess_typeru  r   r7   )rw   r  r  rE   r  rQ  rb  	safe_pathr  guessed_r  r  r   s                 r   rW  z&PhotonAdapter._sidecar_send_attachmentc  sp     * 55c$ii@@	 	%Q4%Q%Q     	("--i88JGQ4I#wGGL 
  

  	 DL 	)(D 	&%DO	;++,>EEEEEEEEDD 	; 	; 	;e3q66:::::::::	;$488K3H3HIIIIs   B 
C(CCCr  c           
       K   | j         t          d          | j                             d| j         d| j         | |d| j        id           d {V }|j        dk    r*t          d| d	|j         d
|j        d d                    |                                pi }|	                    d          s(t          d| d|	                    d                     |S )NzPhoton adapter not connectedr   r   r   r   )r   r   r   r   zPhoton sidecar z
 returned : okz reported error: r   )
rs   r   r0  rg   re   rj   r   r   r   r7   )rw   r  r  r   r  s        r   ru  zPhotonAdapter._sidecar_call  s3     $=>>>&++Ed(EE4+=EtEE-t/BC	 , 
 
 
 
 
 
 
 
 s""W$WW$2BWWdiPTQTPToWW   yy{{ bxx~~ 	L$LL'9J9JLL   r.   )rM   r   )rz   r   r   r{   )r   rk   r   r!   )r   rk   r   rk   r   r!   r   r   )r   rk   r   r   )r   rk   r   r!   )r   r   r   r   )r5  r6  r   r   )NN)
rD   rk   r   rk   rH  rI  rJ  rK  r   r   )NNN)rD   rk   rP  rk   rQ  rI  rH  rI  rJ  rK  r   r   )rD   rk   rY  rk   rQ  rI  rH  rI  rJ  rK  r   r   )rD   rk   r`  rk   rQ  rI  rH  rI  rJ  rK  r   r   )rD   rk   rd  rk   rQ  rI  rH  rI  rJ  rK  r   r   )NNNN)rD   rk   rg  rk   rQ  rI  rh  rI  rH  rI  rJ  rK  r   r   )rD   rk   rk  rk   rQ  rI  rH  rI  rJ  rK  r   r   r   )rD   rk   r   r   )rD   rk   r   r   )r   rk   r   rk   )NNr   r$  )rD   rk   r   rk   rH  rI  rJ  r   r~  r   r  r  r   r   )r  rk   r   rk   r   r   )r  rk   r  rk   rE   rI  r  rI  rQ  rI  rb  rk   r   r   )r  rk   r  r   r   r   )$__name__
__module____qualname____doc___MAX_MESSAGE_LENGTHr  ra   staticmethodru   r   r   r   r   r   r   r   r   r   r-  r   rO  rV  r^  rc  rf  rj  rm  rv  rz  r|  rN  r  rM  rW  ru  __classcell__)ry   s   @r   rL   rL      sI         -:
 :
 :
 :
 :
 :
| # # # \#JO O O O
   "0 0 0 0d" " " ",&1 &1 &1 &1PA A A A   "x1 x1 x1 x1x1
 1
 1
 1
fH H H H!5 !5 !5 !5R #'-1O O O O O* "&"&-1
 
 
 
 
 
 
. "&"&-1
 
 
 
 
" "&"&-1
 
 
 
 
" "&"&-1
 
 
 
 
" "&#'"&-1
 
 
 
 
$ "&"&-1
 
 
 
 
? ? ? ? ?? ? ? ?> > > >' ' ' ' #'? ? ? ? ?BJ J J J& ##'!% .J .J .J .J .J .J`       r.   rL   r  rk   r   c                N   | pd                                 } |                     d          rt          j        S |                     d          rt          j        S |                     d          rt          j        S |                     d          rt          j        S t          j        S )NrA   image/zvideo/audio/zapplication/)rl   
startswithr   PHOTOVIDEOAUDIODOCUMENT)r  s    r   r  r    s    JBDx   !  x   !  x   !  ~&& $##r.   .jpgz.pngz.gifz.webp)z
image/jpegz	image/pngz	image/gifz
image/webpz
image/heicz
image/heifz
image/tiff.mp3z.oggz.wav.m4a)z	audio/mp3z
audio/mpegz	audio/oggz	audio/wavzaudio/x-cafr   z	audio/aacr   r   r   rE   r   rI  c                  |                      d          }|sdS 	 t          j        |          }n:# t          t          f$ r&}t
                              d|           Y d}~dS d}~ww xY wddlm}m	}m
}	 |pd                                }|rt          |          j        nd}
	 |                    d          rF|
pt                               |d          }	  |	||          S # t          $ r  |||          cY S w xY w|s|                    d	          r-|
pt                                ||rd
nd          } |||          S  |||          S # t"          $ r'}t
                              d||           Y d}~dS d}~ww xY w)a  Decode a base64-inlined inbound attachment and cache it locally.

    The sidecar inlines the attachment bytes as ``content["data"]`` (base64).
    We decode them and route to the shared media cache by MIME type, returning
    the cached absolute path so the caller can populate ``media_urls`` (which
    the gateway then hands to the model). Returns ``None`` when there are no
    bytes (over the sidecar's inline cap or a failed read) or when caching
    fails, so the caller can fall back to a text marker.
    r  Nz6[photon] failed to decode inbound attachment bytes: %sr   )cache_audio_from_bytescache_document_from_bytescache_image_from_bytesrA   r  r  r  r  r  z2[photon] failed to cache inbound attachment %s: %s)r7   base64	b64decoder   r   r   r   rU  r  r  r  rl   r   suffixr  _IMAGE_EXT_BY_MIME_AUDIO_EXT_BY_MIMEr   )r   rE   r  r   data_b64rz   r   r  r  r  r  exts               r   r  r    s     {{6""H tx((	"   OQTUUUttttt          JBD"&.T$ZZBF??8$$ 	<@.224@@C<--c3777 < < < 10d;;;;;<  	4$//(33 	4 .227ff C *)#s333((d333   KTSVWWWtttttsR   0 A'A""A'#2E C" "C;8E :C;;AE E 
E?E::E?)	thread_idmedia_filesforce_documentpconfigrD   messager  r  Optional[list]r  c          	       K   t           sddiS t          | j        pi                     d          pt	          j        d          t                    }t	          j        d          }|sddiS dt           d| }d	|i}	d }
	 t          j	        d
          4 d {V }|r|
                    | d||d t                   d|	           d {V }|j        dk    r.dd|j         d|j        d d          icd d d           d {V  S |                                pi }|                    d          s+d|                    d          pdicd d d           d {V  S |                    d          }
dd l}|pg D ]8\  }}t!          j        t%          |                    }|st&                              d           D|                    |          \  }}|||rdndd}|r||d<   |
                    | d||	           d {V }|j        dk    r0dd|j         d|j        d d          ic cd d d           d {V  S |                                pi }|                    d          s-d|                    d          pdic cd d d           d {V  S |                    d          p|
}
:	 d d d           d {V  n# 1 d {V swxY w Y   d|
dS # t,          $ r}dd| icY d }~S d }~ww xY w)Nr   r   rP   rQ   rR   zPhoton standalone send requires a running sidecar with PHOTON_SIDECAR_TOKEN set in the environment. Cron processes cannot spawn the sidecar themselves.r   r   r   r   r   r  r  )r   r   r   zsidecar returned r  r  zsidecar reported failurer   r   z-[photon] standalone send skipping unsafe pathr   r   r  r   r  Tr  zPhoton standalone send failed: )r&   r    r6   r7   r)   r*   rd   rf   r   r   r0  r  r   r   r   r  r   r  rk   r   r   r  r   )r  rD   r  r  r  r  porttokenbaser   last_message_idr   r   r  r  
media_pathr  r  r  r  att_bodyr   s                         r   _standalone_sendr  
  s       0.//		"!!.11URY?T5U5U D I,--E 
7
 	
 4*33T33D'/G%)O.@$T222 )	K )	K )	K )	K )	K )	K )	Kf 8#[[NNN%,g>R?R>R6STT# )        
 #s**#%^9I%^%^TYW[X[W[_%^%^_)	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K yy{{(bxx~~ V#TXXg%6%6%T:TU)	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K #'((;"7"7
 (3(9r K K$
H/LSQ[__]]	  NN#RSSS&11)<<
&%'/AGG\, ,
  3+2HZ(#[[---Hg )         #s**#%^9I%^%^TYW[X[W[_%^%^___K)	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	KL yy{{(bxx~~ V#TXXg%6%6%T:TUUUQ)	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	KR #'((;"7"7"J?+K))	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	K )	KV  ??? @ @ @>1>>???????@sv   ?K AK4K AK
K CK0K AKK K5K 
KK KK 
K8'K3-K83K8r   c                    ddl m} |                     ddd t          t          t
          ddgd|j        t          d	t          d
dt          dddd           | 
                    dd|j        |j                   dS )z.Called by the Hermes plugin loader at startup.r   )clirO   ziMessage via Photonc                     t          |           S r   )rL   r<   s    r   <lambda>zregister.<locals>.<lambda>b  s    M#$6$6 r.   r3   r5   zRun: hermes photon setup  (logs in via device flow, creates a Spectrum project, links your phone number, installs the spectrum-ts sidecar).r@   PHOTON_ALLOWED_USERSPHOTON_ALLOW_ALL_USERSu   📱Tu  You are communicating via Photon Spectrum (iMessage). Treat replies like regular text messages — short, friendly, no markdown rendering. Recipient identifiers are E.164 phone numbers; never expose them in responses unless the user asked. Attachments arrive as metadata only.)rE   r  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_hintz1Set up and manage the Photon iMessage integration)rE   helpr  
handler_fnN)rA   r  register_platformr/   r:   r=   gateway_setuprJ   r  r  register_cli_commandregister_clidispatch)ctx_clis     r   registerr  Y  s     #66#'!)+BC$ #)2-0.. !3;  # # #L @"=	      r.   )r   r   r   r   r   r   r  )r0   r   r   r!   )r   r>   )r  rk   r   r   )
r   r   rE   rk   r  rk   r   r!   r   rI  )r  r   rD   rk   r  rk   r  rI  r  r  r  r!   r   r   r  )@r  
__future__r   r   r  r   loggingr)   r   rh   r'   rB  r'  r+  r   r   r   pathlibr   typingr   r   r	   r
   r   r   r&   ImportErrorgateway.configr   r   rU  r   r   r   r   gateway.platforms.helpersr   authr   	getLoggerr  r   rd   rf   r  r   r   __file__parentr+   r   r    r/   r:   r=   rJ   rL   r  r  r  r  r  r  r-   r.   r   <module>r     s   . # " " " " "     				 				        



  ' ' ' ' ' ' ' '       ; ; ; ; ; ; ; ; ; ; ; ; ; ;  LLLOO    4 3 3 3 3 3 3 3            5 4 4 4 4 4 * * * * * *		8	$	$
  # 
   ! tH~~$y0 +"                    ,G G G G G' G G GZ
  
  
  
          4 4 4 4 4 4B  $"& I@ I@ I@ I@ I@ I@^1 1 1 1 1 1s   A& &	A21A2