
    `gj                   ~   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dd	lmZmZ dd
lm Z m!Z!m"Z"m#Z#m$Z$ ddl%m&Z& ddl'm(Z(  ejR                  e*      Z+dZ,dZ-dZ.dZ/dZ0 ee1      jd                  dz  Z3dZ4dZ5dZ6ddgZ7d3dZ8d4dZ9d4dZ:d5dZ;d6dZ<d6dZ=d7dZ>d4d Z? G d! d"e       Z@d8d#ZAd$d%d&d'd$d$d$d(ZBd)d)d*d+d)d,d,d-ZCdd.	 	 	 	 	 	 	 	 	 d9d/ZDdddd0	 	 	 	 	 	 	 	 	 	 	 	 	 d:d1ZEd5d2ZFy# e$ r dZdZY w xY w);u2  
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ProcessingOutcome
SendResult)strip_markdown   )load_project_credentialsiU"  z	127.0.0.1i@  i  i  sidecariX  )zinternal sidecar errorzupstream connect errorzupstream unavailablezconnection droppedzreset reason: overflowupstream_overflowupstream_unavailable      @z#(?<![\w@])@?hermes\s+agent\b[,:\-]?z(?<![\w@])@?hermes\b[,:\-]?c                H    	 t        |       S # t        t        f$ r |cY S w xY wN)int	TypeError
ValueError)valuedefaults     S/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/photon/adapter.py_coerce_portr"   }   s*    5zz" s   
 !!c                     t         syt        j                  t        j                  d      xs d      syt
        dz  j                         syy)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_requirementsr0      s?    <<		"34>?>)113 r/   c                     t         dz  } t         dz  dz  }	 | j                         j                  |j                         j                  kD  S # t        $ r Y yw xY w)a(  True when node_modules exists but is older than the committed lockfile.

    `hermes update` rewrites ``package-lock.json`` when the spectrum-ts pin is
    bumped, but does not reinstall ``node_modules``. npm records the state of
    the last install in ``node_modules/.package-lock.json``; when the top-level
    lockfile is newer than that marker, the install is out of date. This is the
    same signal ``npm ci`` uses. Returns False (do nothing) if either file is
    missing or unreadable, so a first-run or odd filesystem never blocks start.
    zpackage-lock.jsonr&   z.package-lock.jsonF)r,   statst_mtimeOSError)lockfilemarkers     r!   _sidecar_deps_staler7      sY     11HN*-AAF}}''&++-*@*@@@ s   4A 	AAc            
        t        j                  d      } | st        j                  d       yddlm} 	 t        j                  | dgt        t              dddt         |       	      }|j                  dk7  rIt        j                  d
       t        j                  | dgt        t              dddt         |       	      }|j                  dk7  rAt        j                  d|j                  xs |j                  xs dj!                                yt        j#                  d       y# t        j                  $ r t        j                  dt               Y yw xY w)u  Reinstall the sidecar's node_modules from the lockfile (blocking).

    Mirrors ``hermes photon install-sidecar``: ``npm ci`` for an exact,
    reproducible install, falling back to ``npm install`` if the lockfile is
    missing or drifted. Runs the postinstall patch as part of the install.
    Best-effort — a failure here just leaves the (stale) deps in place and the
    normal ``_start_sidecar`` readiness check reports the real error.
    npmz=[photon] cannot reinstall stale sidecar deps: npm not on PATHNr   windows_hide_flagsciTF)cwdcapture_outputtextchecktimeoutcreationflagsz?[photon] sidecar `npm ci` failed; falling back to `npm install`installz9[photon] sidecar dependency reinstall timed out after %ssz0[photon] sidecar dependency reinstall failed: %s z7[photon] sidecar dependencies reinstalled from lockfile)r(   r)   loggerwarninghermes_cli._subprocess_compatr;   
subprocessrunstrr,   _NPM_REINSTALL_TIMEOUT
returncodeTimeoutExpirederrorstderrstdoutstripinfo)r9   r;   results      r!   _reinstall_sidecar_depsrT      s,    ,,u
CVW A $KL!*,.
 !NNQ  ^^i %#.02F& A>]]1fmm1r88:	

 	MN! $$ 	
 	G"	
 		s   BD' '-EEc                   | j                   xs i }|j                  d      xs t        j                  d      }|j                  d      xs t        j                  d      }|r|st	               \  }}t        |xr |      S y)N
project_idPHOTON_PROJECT_IDproject_secretPHOTON_PROJECT_SECRETT)extragetr*   r+   r   bool)cfgrZ   rV   rX   	stored_id
stored_secs         r!   validate_configr`      so    IIOE<(JBII6I,JJYY/0VBII>U4VN^ 8 :	:I,*--r/   c                    t        |       S r   )r`   r]   s    r!   is_connectedrc      s    3r/   c                     t               \  } }| r|sy| |d}t        j                  dd      j                         }|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)rV   rX   PHOTON_HOME_CHANNELrD   PHOTON_HOME_CHANNEL_NAMEHome)chat_idnamehome_channel)r   r*   r+   rQ   )rV   rX   seedhomes       r!   _env_enablementrm      se     ":!;J> *nMD99*B/557DII8&A 
^ Kr/   c                 j    t        j                  dd      j                         j                         dvS )a:  Send agent replies as markdown (spectrum-ts ``markdown()`` builder).

    iMessage renders it natively; other Spectrum platforms degrade to
    readable plain text. On-device rendering can't be unit-tested, so
    ``PHOTON_MARKDOWN=false`` is the kill-switch back to stripped plain
    text without a release.
    PHOTON_MARKDOWNtrue>   0nofalser*   r+   rQ   lowerr.   r/   r!   _markdown_enabledrv     s5     99&/557==? H  r/   c                  X    e Zd ZdZeZd6 fdZed7d       Zd8dZ	d9dZ
ddd:dZd;d	Zd;d
Zd;dZd<dZd=dZd>dZed?d       Zed@d       Zed@d       Zd;dZd;dZdAdZd;dZ	 	 dB	 	 	 	 	 	 	 	 	 dCdZ	 	 	 dD	 	 	 	 	 	 	 	 	 	 	 dE fdZ	 	 	 dD	 	 	 	 	 	 	 	 	 	 	 dFdZ	 	 	 dD	 	 	 	 	 	 	 	 	 	 	 dGdZ	 	 	 dD	 	 	 	 	 	 	 	 	 	 	 dHdZ	 	 	 	 dI	 	 	 	 	 	 	 	 	 	 	 	 	 dJdZ	 	 	 dD	 	 	 	 	 	 	 	 	 	 	 dKdZdLdMdZ dMdZ!d Z"d!Z#dNd"Z$ e%jL                  d#      Z'e(dOd$       Z)	 	 	 	 	 	 dPd%Z*dQd&Z+	 	 	 	 	 	 	 	 dRd'Z,dSd(Z-	 dL	 	 	 	 	 	 	 dTd)Z.	 dL	 	 	 	 	 dUd*Z/dVd+Z0	 	 	 	 	 	 dWd,Z1dXd-Z2dYd.Z3edZd/       Z4	 	 	 	 d[	 	 	 	 	 	 	 	 	 	 	 	 	 d\d0Z5d]d1Z6dddd2d3	 	 	 	 	 	 	 	 	 	 	 	 	 d^d4Z7d_d5Z8 xZ9S )`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.
    c                   t         |   |t        d             |j                  xs i }t	               \  }}t        j                  d      xs |j                  d      xs |xs d| _        t        j                  d      xs |j                  d      xs |xs d| _	        t        |j                  d      xs t        j                  d      t              | _        t        | _        t        j                  d	      xs t        j                   d
      | _        t%        t        j                  dd            j'                         dv| _        t        j                  d      xs t+        j,                  d      xs d| _        t1               | _        d | _        d | _        d | _        d | _        d| _        d | _        d| _         i | _!        i | _"        i | _#        i | _$        |j                  d      }|t        j                  d      }t%        |      jK                         j'                         dv | _&        | jO                  d|v r|d         | _(        y t        j                  d            | _(        y )NphotonrW   rV   rD   rY   rX   sidecar_portPHOTON_SIDECAR_PORTPHOTON_SIDECAR_TOKEN   PHOTON_SIDECAR_AUTOSTARTrp   )rq   rs   rr   r$   r%   F      .@require_mentionPHOTON_REQUIRE_MENTION>   1onyesrp   mention_patternsPHOTON_MENTION_PATTERNS))super__init__r   rZ   r   r*   r+   r[   _project_id_project_secretr"   _DEFAULT_SIDECAR_PORT_sidecar_port_DEFAULT_SIDECAR_BIND_sidecar_bindsecrets	token_hex_sidecar_tokenrJ   ru   _autostart_sidecarr(   r)   	_node_binrv   supports_code_blocks_sidecar_proc_sidecar_supervisor_task_inbound_task_sidecar_health_task_inbound_running_http_client_sidecar_health_interval_seen_messages_sent_message_ids_last_inbound_by_chat_typing_last_sentrQ   r   _compile_mention_patterns_mention_patterns)selfconfigrZ   r^   r_   _require_mention	__class__s         r!   r   zPhotonAdapter.__init__  sX   (!34"
 !9 :	:II)* yy& 	 	 II-. yy)* 	 	 *IIn%I3H)I!
 3II,-F1B1B21F 	 #&II0&9#

%'-#. #45Wf9MWQW %6$7! :>@D%59<@! %;?(,% 13 46 68"35
 !99%67#!yy)AB"#34::<BBD I
  
 "&!?!?!U* $%"
 45"
r/   c                   | t        t              }nt        | t              r| j	                         }	 |rt        j                  |      ng }t        |t               r|nC|j                         D cg c](  }|j                  d      D ]  }|j	                          * c}}}nt        | t               r| }n| g}g }|D ]R  }t        |      j	                         }|s	 |j                  t        j                  |t        j                               T |S # t        $ r d}Y w xY wc c}}w # t        j                  $ r!}t        j!                  d||       Y d}~d}~ww xY w)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.
        N,z'[photon] Invalid mention pattern %r: %s)list_DEFAULT_MENTION_PATTERNS
isinstancerJ   rQ   jsonloads	Exception
splitlinessplitappendrecompile
IGNORECASErN   rE   rF   )	rawpatternsr?   loadedlinepartcompiledpatternexcs	            r!   r   z'PhotonAdapter._compile_mention_patternsh  sJ    ;56HS!99;D-1D)r ",FD!9v OO-@ JJsO@  

@@H
 T"HuH') 	UGw<%%'DU

4 ?@	U +  @" 88 UH$PSTTUs/   D 2-D&3D,D#"D#,E ?EE c                ^    r| j                   syt        fd| j                   D              S )NFc              3  @   K   | ]  }|j                          y wr   )search).0r   r?   s     r!   	<genexpr>zBPhotonAdapter._message_matches_mention_patterns.<locals>.<genexpr>  s     NG7>>$'N   )r   any)r   r?   s    `r!   !_message_matches_mention_patternsz/PhotonAdapter._message_matches_mention_patterns  s(    411Nt7M7MNNNr/   c                    |s|S | j                   D ][  }|j                  |j                               }|s%|j                         |j                         d j                  d      }|xs |c 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 ,:-)r   matchlstripend)r   r?   r   r   cleaneds        r!   _clean_mention_textz!PhotonAdapter._clean_mention_text  sl     K-- 	'GMM$++-0E++-		5<<VD$&		'
 r/   F)is_reconnectc               F  K   t         s| j                  ddd       y| j                  r| j                  s| j                  ddd       yt	        j
                  d      }|| _        | j                  r	 | j                          d {    nt        j                  d       d| _        t        j                         j!                  | j#                               | _        t        j                         j!                  | j'                               | _        | j+                          t        j-                  d| j.                  | j0                         y7 # t        $ rA}| j                  d	d
| d       |j                          d {  7   d | _        Y d }~yd }~ww xY ww)NMISSING_DEPhttpx not installedF	retryableMISSING_CREDENTIALSzRPHOTON_PROJECT_ID and PHOTON_PROJECT_SECRET are required. Run: hermes photon setup      >@rA   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_errorr   r   httpxAsyncClientr   r   _start_sidecarr   acloserE   rF   r   asyncioget_event_loopcreate_task_inbound_loopr   _monitor_sidecar_healthr   _mark_connectedrR   r   r   )r   r   clientes       r!   connectzPhotonAdapter.connect  s    !!4 "  t';';!!%+	 "  ""40" ""
))+++ NNV
 !%$335AA 
 %,$:$:$<$H$H((*%
! 	R 2 2	
 ; , %%$6qc:" & 
 mmo%%$(!sO   A2F!5E E	E CF!E 	F*FF
FF!FF!c                  K   d| _         | j                  D| j                  }d | _        |j                          |t        j                         ur	 | d {    | j                  6| j                  j                          	 | j                   d {    d | _        | j                          d {    | j                  *	 | j                  j                          d {    d | _	        | j                          y 7 # t        j
                  $ r Y t        $ r Y w xY w7 # t        j
                  $ r Y t        $ r Y w xY w7 7 h# t        $ r Y qw xY ww)NF)r   r   cancelr   current_taskCancelledErrorr   r   _stop_sidecarr   r   _mark_disconnected)r   tasks     r!   
disconnectzPhotonAdapter.disconnect  sN     %$$0,,D(,D%KKM7//11JJ
 )%%'((((
 "&D  """(''..000 !%D!+ --   
 )))   	# 1 s   AEC= C;C= &ED" D D" E/E0EE	 EE	 #E;C= =DEDEDE D" "E5E7E?EEEE	 		EEEEc                J  K   | j                   }|yd| j                   d| j                   d}d| j                  i}d}| j                  r	 |j                  d||d      4 d{   }|j                  d	k7  rt        d
|j                         d}|j                         2 3 d{   }| j                  s n-|j                         }|s*| j                  |       d{    Dddd      d{    | j                  ryy7 7 `7 (6 '7 # 1 d{  7  sw Y   +xY w# t        j                  $ r  t        $ r_}| j                  sY d}~yt        j                  d||       t        j                   |       d{  7   t#        |dz  d      }Y d}~d}~ww xY ww)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headersrA      z/inbound returned z;[photon] inbound stream dropped (%s); reconnecting in %.1fs   r   )r   r   r   r   r   streamstatus_codeRuntimeErroraiter_linesrQ   _on_inbound_liner   r   r   rE   rF   sleepmin)r   r   urlr   backoffrespr   r   s           r!   r   zPhotonAdapter._inbound_loop  s     "">**+1T-?-?,@I+T-@-@A##1!==3 )  : :''3.*-?@P@P?Q+RSS!G&*&6&6&8 : :d#44!#zz|#$"33D999: : ##:: : '9: : : : ))  1,,Qw mmG,,,gk401s   A	F#D% %D&D% ):D#D'D(D+6D!D
"D'D% 2D3D% 7F#F#D% D
DDD% D"DD"D% %F FF#/FFFF#F  F#c                p  K   | j                   r2t        j                  | j                         d{    | j                   sy	 | j	                  di        d{   }t        |t              r|j                  d      nd}t        |t              r|j                  d      durt        |j                  d      xs d      }|j                  d	      }t        |j                  d
      xs d      }d| d| d| }t        j                  d|       | j                  d|d       	 | j                          d{    yy7 7 # t        j
                  $ r  t        $ r!}t        j                  d|       Y d}~}d}~ww xY w7 K# t        $ r }t        j!                  d|       Y d}~yd}~ww xY ww)a  Promote degraded upstream Photon stream health into reconnect.

        The sidecar HTTP process can stay alive while spectrum-ts repeatedly
        fails to maintain the upstream inbound gRPC stream. Polling `/healthz`
        keeps that from becoming a silent inbound outage.
        N/healthzz([photon] sidecar health check failed: %sr   okFstateunknowndegradedForMs	lastIssuezunknown stream issuez'Photon upstream stream degraded (state=z, degradedForMs=z): [photon] %sUPSTREAM_STREAM_DEGRADEDTr   ,[photon] fatal-error notification failed: %s)r   r   r   r   _sidecar_callr   r   rE   debugr   dictr[   rJ   rN   r   _notify_fatal_errorrF   )r   datar   r   r  degraded_for_ms
last_issuemessages           r!   r   z%PhotonAdapter._monitor_sidecar_health"  s     ##-- = =>>>((!//
B?? ,6dD+ATXXh'tFfd+vzz$/?u/L

7+8y9E$jj9OVZZ4N8NOJ '!1/1B#,  
 LL0!!* " 
T..000 E $> @))  GM. 1 TMsSSTs   0F6EF6E EE C	F6)F
 <F=F
 F6E F$F :F6 FF6F
 
	F3F.)F6.F33F6c                ^  K   	 t        j                  |      }|j                  d      }|r| j                  |      ry 	 | j                  |       d {    y # t         j                  $ r t        j	                  d       Y y w xY w7 3# t        $ r t        j                  d       Y y w xY ww)Nz'[photon] skipping non-JSON inbound line	messageIdz [photon] inbound dispatch failed)
r   r   JSONDecodeErrorrE   r	  r[   _is_duplicate_dispatch_inboundr   	exception)r   r   eventmsg_ids       r!   r   zPhotonAdapter._on_inbound_lineM  s     	JJt$E ;'d((0	A((/// ## 	LLBC	 0 	A?@	As\   B-A %B-B	 BB	 B-(BB-BB-B	 	B*'B-)B**B-c                (   t        j                          }| j                  }|j                  |      }|||z
  t        k  ry||v r||= |||<   t	        |      t
        kD  r4t        |j                               d t	        |      t
        z
   D ]  }||=  y)NTF)timer   r[   _DEDUP_WINDOW_SECONDSlen_DEDUP_MAX_SIZEr   keys)r   r  nowseentolds         r!   r  zPhotonAdapter._is_duplicate[  s    iik""HHV=S1W'<< T>VVt9&DIIK()F3t9+FG Ir/   c                	  K   |j                  d      xs i }|j                  d      xs i }|j                  d      xs i }|j                  d      xs d}|st        j                  d       y|j                  d      d	k(  rd	nd
}|j                  d      xs |j                  d      xs |}|j                  d      xs d}	 |r%t        j                  |j                  dd            n#t        j                  t        j                        }	g }
g }	 	 	 	 d(d}|j                  d      }|dk(  r|j                  d      }|j                  d      dk(  xs |xr || j                  v }|st        j                  d       y|j                  d      xs d}| j                  |||||xs d      }| j                  t        d| t        j                   ||j                  d      ||j                  d      xs dd||		             d{    y| j#                  ||j                  d             |dk(  r'|j                  d      xs d}t        j                   }n|dv r ||      \  }}}
}n|d	k(  rjg }t        j                   }|j                  d      xs g D ]  }t%        |t&              s|j                  d      xs i }t%        |t&              s:|j                  d      }|dk(  r)|j                  d      xs d}|r|j)                  |       y|dv rX ||      \  }}}}|t        j                   k(  r|}|
j+                  |       |j+                  |       |s|j)                  |       |s|j)                  d | d!        |
r#|t        j                   k(  rt        j,                  }d"j/                  d# |D              j1                         }|s|
rd$nd%}nd | d!}t        j                   }|d	k(  rD| j2                  r8| j5                  |      st        j                  d&       y| j7                  |      }| j                  |||||xs d      }t        ||||j                  d      ||	|
|'      }| j                  |       d{    y# t        $ r( t        j                  t        j                        }	Y w xY w7 7 <w))uq  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"?}
                       | {"type": "reaction", "emoji": "❤️",
                          "targetMessageId": "..." | null,
                          "targetDirection": "inbound"|"outbound" | null,
                          "targetText": "..." | null},
              "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idrD   z![photon] inbound missing space.idNtypegroupdmphone	timestampZz+00:00)tzc                   | j                  d      dk(  }| j                  d      xs |rdnd}| j                  d      xs d}|rt        j                  n
t        |      }t	        | |||      }|r|rdnd	||g|xs |rd
gfS dgfS |rdnd}| j                  d      }t        |t        t        f      rd| dnd}d| d| d|xs d | d|g g fS )Nr'  voiceri   z	(unnamed)mimeTyperD   force_audioz(voice)(attachment)	audio/mp4zapplication/octet-stream
attachmentdurationz, duration: sz[Photon z received: z (zunknown MIMEz)])r[   r   VOICE_attachment_message_type_cache_inbound_attachmentr   r   float)	payloadis_voiceri   mimemtypecachedlabelr6  duration_texts	            r!   _normalize_binary_payloadzBPhotonAdapter._dispatch_inbound.<locals>._normalize_binary_payload  s$    {{6*g5H;;v&Ph7KD;;z*0bD)1K%%7OPT7UE.tF !)I~HVXkW	  <VW	   (G\E{{:.H he5 xj*  5'TF 3*N+M?"> r/   reactiontargetMessageIdtargetDirectionoutboundz6[photon] ignoring reaction on a message we didn't sendemoji)rh   	chat_name	chat_typeuser_id	user_namezreaction:added:r  
targetTextT)	r?   message_typesource
message_idreply_to_message_idreply_to_textreply_to_is_own_messageraw_messager+  r?   >   r/  r5  itemsz"[Photon content type not handled: ]
c              3  &   K   | ]	  }|s|  y wr   r.   )r   r   s     r!   r   z2PhotonAdapter._dispatch_inbound.<locals>.<genexpr>  s     AdDTAs   r3  z[Photon empty group received]zR[photon] ignoring group message (require_mention=true, no mention pattern matched))r?   rN  rO  rP  rT  r+  
media_urlsmedia_types)r<  Dict[str, Any]returnz-tuple[str, MessageType, List[str], List[str]])r[   rE   rF   r   fromisoformatreplacer  r   utcr   r   r	  build_sourcehandle_messager   r   TEXT_record_last_inboundr   r
  r   extendDOCUMENTjoinrQ   r   r   r   )r   r  r#  r$  r%  space_idrJ  	sender_idts_strr+  rY  rZ  rC  ctype	target_idis_oursrH  rO  r?   r?  
text_partsitemitem_content	item_type	item_textr6   
item_mtype	item_urls
item_typesmessage_events                                 r!   r  zPhotonAdapter._dispatch_inboundl  s    4 		'"(b8$*))I&,"99T?(bNN>?  %yy0G;G	JJt$F		'(:Fh	;'-2	6  &&v~~c8'DE\\X\\2  !#
!#	#	:	@ F#J
  $56Ikk"34
B Ai4+A+AA  L KK(.BE&& "#!#+t ' F %%*5'2!,!1!1!$yy5(1")++l";"Ct,0 %'
   
 	!!(EIIk,BCF?;;v&,"D$$E--3LW3U0D%[g$&J$$EG,2 Y!$-#xx	28b!,5(,,V4	& , 0 0 8 >BI ")))4 77@Y$A=FJ	:  0 00 *%%i0&&z2$"))&1%%(J9+UV&WX1Y2 e{'7'77#,,99AjAAGGID)3~9X7wa@D$$E D$8$899$?I ++D1D""'4 # 
 %yy-!#	
 !!-000C  	6 5I	6Lv 	1sR   CSAR C;SS
E)S6DSSS-SSSSSc                2   	 t        j                  ddd|  dgdddd      }|j                  j                         D cg c],  }|j                         j                         s"t        |      . c}S # t        t         j                  f$ r g cY S w xY wc c}w )	zBPIDs listening on a local TCP port (empty if none/undeterminable).lsofz-tiztcp:z-sTCP:LISTENTr   Fr>   r?   rA   r@   )	rH   rI   r4   rM   rP   r   rQ   isdigitr   )portouttoks      r!   _find_listener_pidsz!PhotonAdapter._find_listener_pids?  s    	..$tf~>#$5C %(JJ$4$4$6PS#))+:M:M:OCPP 223 	I	Ps   !A3 #B#B3BBc                    	 t        j                  ddt        |       ddgdddd      }d	|j
                  v S # t        t         j                  f$ r Y yw xY w)
z;True if ``pid``'s command line is a Photon sidecar process.psz-pz-ozcommand=Tr   Frx  zphoton/sidecar/index.mjs)rH   rI   rJ   r4   rM   rP   )pidr{  s     r!   _pid_is_sidecarzPhotonAdapter._pid_is_sidecarK  s`    	..tSXtZ8#$5C *SZZ77 223 		s   (8 AAc                P    	 t        j                  | d       y# t        $ r Y yw xY w)Nr   TF)r*   killr4   )r  s    r!   
_pid_alivezPhotonAdapter._pid_aliveX  s)    	GGCO 		s    	%%c                j   K   t         j                  dk(  ry	 t        j                  d      4 d{   }|j	                  d j
                   d j                   dd j                  i	       d{    ddd      d{     j                   j                        }|D cg c]  } j                  |      s| }}|D cg c]	  }||vs| }}|s t        d
 j                   d|xs d d      |D ]H  }t        j                  d| j                         	 t        j                  |t         j"                         J t'        j&                         dz   }t'        j&                         |k  r]t)         fd|D              rIt+        j,                  d       d{    t'        j&                         |k  rt)         fd|D              rI|D ]9  } j/                  |      s	 t        j                  |t         j0                         ; t+        j,                  d       d{    |rt        d
 j                   d| d      y7 7 7 # 1 d{  7  sw Y   xY w# t        j                  $ r Y yw xY wc c}w c c}w # t$        $ r Y w xY w7 # t$        $ r Y w xY w7 w)u:  Kill an orphaned sidecar squatting our port before spawning ours.

        A hard gateway exit (crash, SIGKILL, supervisor restart) used to leave
        the detached sidecar running with a token the new gateway doesn't
        know, so it can't be told to ``/shutdown`` — and every replacement
        spawn died on EADDRINUSE, failing each reconnect attempt. The
        stdin-EOF watch prevents new orphans; this reclaims the port from
        orphans that predate it (or survived it). Listeners are verified by
        command line before being signalled.
        win32N       @r   r   r   r   r   r   zport z% is in use by another process (pids: r  uR   , not a Photon sidecar) — free it or set PHOTON_SIDECAR_PORT to a different portz5[photon] reaping orphaned sidecar (pid %d) on port %d      @c              3  @   K   | ]  }j                  |        y wr   )r  )r   pr   s     r!   r   z4PhotonAdapter._reap_stale_sidecar.<locals>.<genexpr>  s     ,OAT__Q-?,Or   g?皙?z. is also held by non-sidecar processes (pids: u<   ) — free it or set PHOTON_SIDECAR_PORT to a different port)sysplatformr   r   postr   r   r   RequestErrorr}  r  r   rE   rF   r*   r  signalSIGTERMr4   r  r   r   r   r  SIGKILL)r   r   pidsr  staleforeigndeadlines   `      r!   _reap_stale_sidecarz!PhotonAdapter._reap_stale_sidecar`  s     <<7"	((5  kkd00143E3E2FhO5t7J7JK "     ''(:(:; $B(<(<S(ABB"&;3#U*:3;;**+ ,!.Y/ 0IJ 
  	CNNGT''V^^,	 99;$iikH$,O,O)O--$$$ iikH$,O,O)O 	Cs#GGC0	 mmC   **+ ,$$+9 -:;  I   
 !! 		 C;   %
   	!s  J3I- II- >I5I6I:I- II- 
J3)J JJ3
	JJA
J3#$JAJ3#J $/J3J3,$J"J3*J1+#J3I- II- I*I I*%I- -J J3JJ3	JJ3JJ3"	J.+J3-J..J3c           
       K   t         dz  j                         st        dt          d      t               r6t        j                  d       t        j                  t               d {    | j                          d {    t        j                  j                         }| j                  |d<   | j                  |d<   t        | j                         |d<   | j"                  |d<   | j$                  |d	<   d
|d<   ddlm} 	 t+        j,                  | j.                  t        t         dz        t        t               gdddd |             }|j0                  dk7  r5t        |j2                  xs |j4                  xs dj7                               |j2                  j7                         r.t        j9                  d|j2                  j7                                t+        j<                  | j.                  t        t         dz        gt*        j>                  t*        j>                  t*        j@                  |tB        jD                  dk7   |             | _#        t        jH                         }|jK                  | jM                  | jF                              | _'        tQ        jP                         dz   }d }tS        jT                  d      4 d {   }tQ        jP                         |k  r| jF                  jW                         #t        d| jF                  j0                   d      	 |jY                  d| j"                   d| j                    d d!| j$                  i"       d {   }	|	jZ                  d#k(  r	 d d d       d {    y 	 t        j^                  d$       d {    tQ        jP                         |k  rd d d       d {    t        d%|       7 u7 `# t:        $ r!}t        j                  d|       Y d }~/d }~ww xY w7 D7 7 # tR        j\                  $ r}
|
}Y d }
~
d }
~
ww xY w7 7 p# 1 d {  7  sw Y   xY ww)&Nr&   z+Photon sidecar deps not installed. Run: cd z, && npm install   (or `hermes photon setup`)zX[photon] sidecar deps are stale (lockfile newer than install); reinstalling before startrW   rY   r|   PHOTON_SIDECAR_BINDr}   r   PHOTON_SIDECAR_WATCH_STDINr   r:   z$patch-spectrum-mixed-attachments.mjsT
   F)r>   r?   rA   r@   rB   rD   r  z<[photon] failed to apply Spectrum mixed attachment patch: %sz	index.mjsr  )stdinrP   rO   envstart_new_sessionrB   r   r  r   z Photon sidecar exited with code z before becoming readyr   r   r   r   r  r   r  z0Photon sidecar did not become ready within 15s: )0r,   r-   r   r7   rE   rF   r   	to_threadrT   r  r*   environcopyr   r   rJ   r   r   r   rG   r;   rH   rI   r   rL   rO   rP   rQ   r	  r   PopenPIPESTDOUTr  r  r   r   r   _supervise_sidecarr   r  r   r   pollr  r   r  r   )r   r  r;   patchr   loopr  last_errr   r   r   s              r!   r   zPhotonAdapter._start_sidecar  s    ~-557"^#OQ   NN, ##$;<<<&&(((jjoo#'#3#3 '+';';#$%(););%<!"%)%7%7!"&*&9&9"# -0() 	E	NNNN'MMN%
  $ 12E 1$"ELL$FELL$FB#M#M#OPP||!!#]ELL,>,>,@A (--^^S!;<=//??$$"||w6 -.
 %%'(,(8(8##D$6$67)
%
 99;%(,$$S1 	) 	)V))+(%%**,8&:--8899OQ !!'!$"4"4!5Qt7I7I6J(S!94;N;N O "- " D ''3.	) 	) 	) / mmC((( ))+(	) 	)" >xjI
 	
e =(H  	NNN 	8	)	) )) ! H!(!	) 	) 	) 	)s   A&Q(O)Q O BQCO# C3QPQAP=&=P#P$P8QPQ	P="P9#P=?Q
P;Q Q#	P,PQPQPQP6*P1,P=1P66P=;Q=QQQQc                  K   |j                   y|j                   }t        j                         }	 	 |j                  d|j                         d{   }|sn5t
        j                  d|j                  dd      j                                \	 | j                  rX|j                         }t
        j                  d|       | j                  dd	| d
d       	 | j                          d{    yy7 # t        $ r }t
        j                  d|       Y d}~d}~ww xY w7 4# t        $ r }t
        j                  d|       Y d}~yd}~ww xY ww)z1Pump the sidecar's stdout/stderr into our logger.NTz[photon-sidecar] %szutf-8r^  z&[photon-sidecar] supervisor exited: %suG   [photon] sidecar exited unexpectedly (code %s) — triggering reconnectSIDECAR_CRASHEDz)Photon sidecar exited unexpectedly (code )r   r  )rP   r   r   run_in_executorreadlinerE   rR   decoderstripr   rF   r   r  rN   r   r  )r   procrP   r  r   r   	exit_coder   s           r!   r  z PhotonAdapter._supervise_sidecar  s?    ;;%%'	H!11$HH14;;w	3R3Y3Y3[\	     		ILLY !!!;I;aH " 
T..000 ! I  	HNNCQGG	H 1 TMsSSTs}   .E C6 C4<C6 AED$ -D".D$ 2E4C6 6	D?DEDE"D$ $	E-EEEEc                6  K   | j                   }|y 	 |j                  	 |j                  j                          | j                  N	 | j                  j                  d| j                   d| j                   dd| j                  id       d {    	 |j                  d       d | _         | j0                  "| j0                  j3                          d | _        y y # t        $ r Y w xY w7 \# t        $ r Y ew xY w# t        j                  $ r t        j                  d	k7  rh	 t        j                  t        j                   |j"                        t$        j&                         n5# t(        t*        f$ r |j-                          Y nw xY w|j-                          	 |j                  d       n)# t        j                  $ r |j/                          Y nw xY wY 4w xY w# d | _         | j0                  "| j0                  j3                          d | _        w w xY ww)
Nr   r   z	/shutdownr   r  r   r  r   r  )r   r  closer   r   r  r   r   r   waitrH   rM   r  r  r*   killpggetpgidr  r  r  ProcessLookupErrorPermissionError	terminater  r   r   )r   r  s     r!   r   zPhotonAdapter._stop_sidecar  s    !!<%	5 zz%JJ$$&   ,++00!$"4"4!5Qt7I7I6J)T!94;N;N O # 1    		#	& "&D,,8--44604- 99 ! 

 !  ,,  <<7*)		"**TXX"6G.@ )() NN$ IIcI*!00  IIK   "&D,,8--44604- 9s   HG C G AC. C,C. C= '6H	C)&G (C))G ,C. .	C:7G 9C::G =&G$AE&%G&FGFGF/.G/#GGGGG GG 7HHNc                `   K   | j                  || j                  |             d {   S 7 wr   )_sidecar_sendformat_message)r   rh   r%  reply_tometadatas        r!   sendzPhotonAdapter.sendL  s,      ''1D1DW1MNNNNs   %.,.c                   K   	 ddl m}  ||       d {   }| j                  |||       d {   S 7  # t        $ r t        |   ||||       d {  7  cY S w xY w7 0w)Nr   )cache_image_from_urlcaption)gateway.platforms.baser  r   r   
send_image_sidecar_send_attachment)	r   rh   	image_urlr  r  r  r  
local_pathr   s	           r!   r  zPhotonAdapter.send_image]  sz     	SC3I>>J 22Z 3 
 
 	
	 ? 	S+GYRRRR	S
sG   A%9 79 A%A#A%9 A AA A%A  A%c                F   K   | j                  |||       d {   S 7 wNr  r  )r   rh   
image_pathr  r  r  kwargss          r!   send_image_filezPhotonAdapter.send_image_filep  1      22Z 3 
 
 	
 
   !!c                H   K   | j                  |||d       d {   S 7 w)Nr/  )r  kindr  )r   rh   
audio_pathr  r  r  r  s          r!   
send_voicezPhotonAdapter.send_voice}  s3      22Zw 3 
 
 	
 
   " "c                F   K   | j                  |||       d {   S 7 wr  r  )r   rh   
video_pathr  r  r  r  s          r!   
send_videozPhotonAdapter.send_video  r  r  c                H   K   | j                  ||||       d {   S 7 w)N)ri   r  r  )r   rh   	file_pathr  	file_namer  r  r  s           r!   send_documentzPhotonAdapter.send_document  s3      22YY 3 
 
 	
 
r  c                H   K   | j                  |||||       d {   S 7 wr   )r  )r   rh   animation_urlr  r  r  s         r!   send_animationzPhotonAdapter.send_animation  s.      __]GXx
 
 	
 
r  c                8  K   t        j                          }|| j                  j                  |d      z
  t        k  ry || j                  |<   	 | j	                  d|dd       d {    y 7 # t
        $ r }t        j                  d|       Y d }~y d }~ww xY ww)Ng        /typingstartspaceIdr  z[photon] send_typing failed: %s)r  r   r[   _TYPING_COOLDOWN_SECONDSr  r   rE   r	  )r   rh   r  r  r   s        r!   send_typingzPhotonAdapter.send_typing  s     iik''++GS99<TT*-w'	?$$wA    	?LL:A>>	?sB   ABA. &A,'A. +B,A. .	B7BBBBc                   K   | j                   j                  |d        	 | j                  d|dd       d {    y 7 # t        $ r }t        j                  d|       Y d }~y d }~ww xY ww)Nr  stopr  z[photon] stop_typing failed: %s)r   popr  r   rE   r	  )r   rh   r   s      r!   stop_typingzPhotonAdapter.stop_typing  sf     ""7D1	?$$w@    	?LL:A>>	?s;   A,A  >A  A,A   	A)	A$A,$A))A,i  r   c                   |sy | j                   }||v r||= t        j                         ||<   t        |      | j                  kD  r;t	        |j                               d t        |      | j                  z
   D ]  }||=  y y r   )r   r  r  _SENT_IDS_MAXr   r  )r   rP  sentr!  s       r!   _record_sent_messagez"PhotonAdapter._record_sent_message  s    %%Z 99;Zt9t)))DIIK()I3t9t7I7I+IJ I *r/   z^any;-;(\+\d{6,})$c                b    | j                   j                  |      }|r|j                  d      S |S )Nr   )_DM_CHAT_GUID_REr   r(  )clsrh   r   s      r!   _normalize_chat_keyz!PhotonAdapter._normalize_chat_key  s-    $$**73!&u{{1~3G3r/   c                   |r|sy | j                  |      }| j                  }||v r||= |||<   t        |      | j                  kD  r;t	        |j                               d t        |      | j                  z
   D ]  }||=  y y r   )r  r   r  _LAST_INBOUND_CHATS_MAXr   r  )r   rh   rP  keylastr!  s         r!   rc  z"PhotonAdapter._record_last_inbound  s     j&&w/))$;S	S	t9t333DIIK(:#d)d:::  I 4r/   c                j    t        j                  dd      j                         j                         dv S )NPHOTON_REACTIONSrs   >   r   r   r   rp   rt   )r   s    r!   _reactions_enabledz PhotonAdapter._reactions_enabled  s3    yy+W5;;=CCE J
 
 	
r/   c                   K   	 | j                  d|||d       d{    y7 # t        $ r }t        j                  d|       Y d}~yd}~ww xY ww)zCTapback ``emoji`` onto a message. Soft-fails (False), never raises.z/react)r  r  rH  NTz [photon] add_reaction failed: %sFr  r   rE   r	  )r   rh   rP  rH  r   s        r!   _add_reactionzPhotonAdapter._add_reaction  s]     	$$#*uM   	
  	LL;Q?	s6   A% #% A% 	AA	A	AAc                   K   	 | j                  d||d       d{    y7 # t        $ r }t        j                  d|       Y d}~yd}~ww xY ww)a+  Retract our tapback from a message. Soft-fails (False), never raises.

        The sidecar tracks one reaction handle per target message; after a
        sidecar restart the handle is gone and removal is best-effort (the
        stale tapback self-heals when the next reaction replaces it).
        z/unreact)r  r  NTz#[photon] remove_reaction failed: %sFr  )r   rh   rP  r   s       r!   _remove_reactionzPhotonAdapter._remove_reaction
  sZ     	$$jI     	LL>B	s6   A$ "$ A$ 	AAAAAc                   K   |xs* | j                   j                  | j                  |            }|sdddS | j                  |||       d{   }|sdddS d|dS 7 w)uA  Tapback ``emoji`` onto a message in ``chat_id``.

        Without ``message_id``, targets the chat's most recent inbound
        message (typically the one the agent is responding to). iMessage
        maps ❤️👍👎😂‼️❓ to native tapbacks; anything else uses Apple's
        custom-emoji reaction.
        Fuk   no message to react to — pass message_id (no inbound message seen in this chat since the gateway started)successrN   Nz'reaction failed (see gateway debug log)Tr  rP  )r   r[   r  r  )r   rh   rH  rP  targetr   s         r!   add_reactionzPhotonAdapter.add_reaction   s       
t99==$$W- 
  O 
 %%gvu== B   v66 >s   AA!AA!c                   K   |xs* | j                   j                  | j                  |            }|sdddS | j                  ||       d{   }|sdddS d|dS 7 w)z1Retract our tapback from a message (best-effort).Fu)   no message to unreact — pass message_idr  Nz&unreact failed (see gateway debug log)Tr  )r   r[   r  r   )r   rh   rP  r  r   s        r!   remove_reactionzPhotonAdapter.remove_reaction>  s       
t99==$$W- 
  D  ((&99 A   v66 :s   AA AA c                   K   | j                         syt        |j                  dd      }t        |dd      }|r|r| j                  ||d       d{    yyy7 w)u=   Tapback 👀 on the triggering message while the agent works.Nrh   rP  u   👀)r  getattrrO  r  )r   r  rh   rP  s       r!   on_processing_startz!PhotonAdapter.on_processing_startR  s\     &&(%,,	48UL$7
z$$Wj,GGG "7Gs   AAAAc                |  K   | j                         syt        |j                  dd      }t        |dd      }|r|sy| j                  ||       d{    |t        j
                  k(  r| j                  ||d       d{    y|t        j                  k(  r| j                  ||d       d{    yy7 c7 77 
w)u  Swap the 👀 progress tapback for a 👍/👎 result.

        Remove-then-add rather than a bare replace: deterministic whether the
        platform replaces a sender's previous tapback or stacks them, and it
        keeps the sidecar's reaction-handle slot coherent.
        Nrh   rP  u   👍u   👎)r  r
  rO  r   r   SUCCESSr  FAILURE)r   r  outcomerh   rP  s        r!   on_processing_completez$PhotonAdapter.on_processing_complete[  s      &&(%,,	48UL$7
j##GZ888'///$$Wj,GGG)111$$Wj,GGG 2 	9GGs6   AB<B6-B< B8.B</B:0B<8B<:B<c                   K   |d|dS w)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)  )ri   r'  r&  r.   )r   rh   s     r!   get_chat_infozPhotonAdapter.get_chat_infoq  s       W==s   	c                0    t               r|S t        |      S r   )rv   r   )r   r%  s     r!   r  zPhotonAdapter.format_messagey  s     Ng&&r/   c                    t        j                  |       ry| sy| j                         t        fdt        D              S )NTFc              3  &   K   | ]  }|v  
 y wr   r.   )r   patlowereds     r!   r   z4PhotonAdapter._is_retryable_error.<locals>.<genexpr>  s     Hc3'>Hs   )r   _is_retryable_errorru   r   _PHOTON_RETRYABLE_PATTERNS)rN   r  s    @r!   r  z!PhotonAdapter._is_retryable_error  s7    2259++-H-GHHHr/   c                  K   | j                  |      }| j                  ||||       d{   }|j                  r|S |j                  xs d}	|j                  xs | j                  |	      }
|
s| j                  |	      r|S |
rt        d|dz         D ]  }|d|dz
  z  z  }t        j                  d||||	       t        j                  |       d{    | j                  ||||       d{   }|j                  r|c S |j                  xs d}	|j                  r| j                  |	      r n t        j                  d||	       |S t        j                  d|	       | j                  ||d| j                   ||       d{   }|j                  s t        j                  d	|j                         |S 7 7 7 7 9w)
u   Retry sends without the generic Markdown banner.

        Photon replies are markdown (rendered by iMessage) or stripped plain
        text under ``PHOTON_MARKDOWN=false`` — either way the gateway's
        generic banner never applies.
        )rh   r%  r  r  NrD   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)r  r  r  rN   r   r  _is_timeout_errorrangerE   rF   r   r   MAX_MESSAGE_LENGTH)r   rh   r%  r  r  max_retries
base_delayr?   rS   	error_str
is_networkattemptdelayfallback_results                 r!   _send_with_retryzPhotonAdapter._send_with_retry  s     ""7+yy	 ! 
 
 >>MLL&B	%%L)A)A))L
d44Y?M K!O4 "aGaK&89Q[% mmE***#yy# %%	  )    >>!M"LL.B	((D,D,DY,O#& N D	
 !%		24223	 !* !
 
 &&LLDoF[F[\i
* +,
sS   *GF>B&GGG0G10G"G4AGG3GGGGc                  K   t        |      | j                  kD  r9t        j                  dt        |      | j                         |d | j                   }||d}t	               rd|d<   	 | j                  d|       d {   }| j                  |j                  d             t        d	|j                  d      
      S 7 @# t        $ r }t        dt        |            cY d }~S d }~ww xY ww)Nz0[photon] truncating outbound from %d to %d charsr  r?   markdownformat/sendFr  r  Tr  )r  r  rE   rF   rv   r  r   r   rJ   r  r[   )r   rg  r?   bodyr  r   s         r!   r  zPhotonAdapter._sidecar_send  s     t9t...NNBD	422 1$112D+3TB 'DN	;++GT::D 	!!$((;"78$488K3HII	 ; 	;e3q6::	;sH   A&C-)C >B??C <C-?C 	C*
C%C* C-%C**C-r5  )ri   	mime_typer  r  c                 K   | j                  t        |            }|st        dd|       S |sddl}|j	                  |      \  }	}
|	xs d}|||dk(  rdndd}|r||d	<   |r||d
<   |r||d<   	 | j                  d|       d{   }| j                  |j                  d             t        d|j                  d            S 7 @# t        $ r }t        dt        |            cY d}~S d}~ww xY ww)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/  r5  r  pathr  ri   r0  r  /send-attachmentr  Tr  )	validate_media_delivery_pathrJ   r   	mimetypes
guess_typer  r   r  r[   )r   rg  r/  ri   r,  r  r  	safe_pathr2  guessed_r+  r  r   s                 r!   r  z&PhotonAdapter._sidecar_send_attachment  s    * 55c$i@	'J4&%Q  "--i8JGQ4I#wGL 

 DL(D%DO	;++,>EED 	!!$((;"78$488K3HII	 F 	;e3q6::	;sH   A/C62C
 CC
 <C6C
 
	C3C.(C3)C6.C33C6c           
     ^  K   | j                   t        d      d| j                   d| j                   | }d| j                  i}t        j                  d      4 d {   }|j                  |||       d {   }d d d       d {    j                  dk7  r+t        d	| d
|j                   d|j                  d d        |j                         xs i }|j                  d      s t        d	| d|j                  d             |S 7 7 7 # 1 d {  7  sw Y   xY ww)NzPhoton adapter not connectedr   r   r   r   r   r   r   r   zPhoton sidecar z
 returned : r   z reported error: rN   )r   r   r   r   r   r   r   r  r   r?   r   r[   )r   r/  r+  r   r   r   r   r  s           r!   r  zPhotonAdapter._sidecar_call  sH    $=>> **+1T-?-?,@G+T-@-@A$$T2 	F 	FfStWEED	F 	Fs"!$z$2B2B1C2diiPTQToEVW  yy{ bxx~!$'8'9J8KL  	FE	F 	F 	F 	Fs[   AD- D!D-$D<D=DD-DBD-DD-D*D!D*&D-)r   r   )r   r   r\  z'list[re.Pattern]')r?   rJ   r\  r\   )r?   rJ   r\  rJ   )r   r\   r\  r\   r\  None)r   rJ   r\  r;  )r  rJ   r\  r\   )r  r[  r\  r;  )rz  r   r\  z	List[int])r  r   r\  r\   )r  zsubprocess.Popenr\  r;  )NN)
rh   rJ   r%  rJ   r  Optional[str]r  Optional[Dict[str, Any]]r\  r   )NNN)rh   rJ   r  rJ   r  r<  r  r<  r  r=  r\  r   )rh   rJ   r  rJ   r  r<  r  r<  r  r=  r\  r   )rh   rJ   r  rJ   r  r<  r  r<  r  r=  r\  r   )rh   rJ   r  rJ   r  r<  r  r<  r  r=  r\  r   )NNNN)rh   rJ   r  rJ   r  r<  r  r<  r  r<  r  r=  r\  r   )rh   rJ   r  rJ   r  r<  r  r<  r  r=  r\  r   r   )rh   rJ   r\  r;  )rP  r<  r\  r;  )rh   rJ   r\  rJ   )rh   r<  rP  r<  r\  r;  r\  r\   )rh   rJ   rP  rJ   rH  rJ   r\  r\   )rh   rJ   rP  rJ   r\  r\   )rh   rJ   rH  rJ   rP  r<  r\  r[  )rh   rJ   rP  r<  r\  r[  )r  r   r\  r;  )r  r   r  r   r\  r;  )rh   rJ   r\  r[  )r%  rJ   r\  rJ   )rN   r<  r\  r\   )NNr   r  )rh   rJ   r%  rJ   r  r<  r  r   r  r   r  r;  r\  r   )rg  rJ   r?   rJ   r\  r   )rg  rJ   r/  rJ   ri   r<  r,  r<  r  r<  r  rJ   r\  r   )r/  rJ   r+  r[  r\  r[  ):__name__
__module____qualname____doc___MAX_MESSAGE_LENGTHr  r   staticmethodr   r   r   r   r   r   r   r   r  r  r}  r  r  r  r   r  r   r  r  r  r  r  r  r  r  r  r  r  r  r   r   r  classmethodr  rc  r  r  r   r  r  r  r  r  r  r  r%  r  r  r  __classcell__)r   s   @r!   rx   rx     s    -J
\ # #JO
" 5: 3j"B&1P)VA"O1f 	Q 	Q 
8 
8  7rf
PT<)5b #'-1OO O  	O
 +O 
O* "&"&-1

 
 	

  
 +
 

. "&"&-1

 
 	

  
 +
 

" "&"&-1

 
 	

  
 +
 

" "&"&-1

 
 	

  
 +
 

" "&#'"&-1

 
 	

 !
  
 +
 

$ "&"&-1

 
 	

  
 +
 


?? M!	  "rzz"784 4$2?	 

(+47	4 %)	77 7 "	7
 
7> 9=77(57	7(HH!H,=H	H,>' I I #'DD D  	D
 D D D 
DLJ0 ##'!% /J/J /J
 /J !/J /J /J 
/Jbr/   rx   c                R   | xs dj                         } | j                  d      rt        j                  S | j                  d      rt        j                  S | j                  d      rt        j
                  S | j                  d      rt        j                  S t        j                  S )NrD   image/zvideo/audio/zapplication/)ru   
startswithr   PHOTOVIDEOAUDIOre  )r>  s    r!   r9  r9  0  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-cafr4  z	audio/aacr1  c                  | j                  d      }|sy	 t        j                  |      }ddlm}m	}m
}	 |xs dj                         }|rt        |      j                  nd}
	 |j                  d      r$|
xs t        j                  |d      }	  |	||      S |s|j                  d	      r'|
xs t         j                  ||rd
nd      } |||      S  |||      S # t        t        f$ r }t
        j                  d|       Y d}~yd}~ww xY w# t        $ r  |||      cY S w xY w# t"        $ r!}t
        j                  d||       Y d}~y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_bytesrD   rH  rN  rI  rP  rO  z2[photon] failed to cache inbound attachment %s: %s)r[   base64	b64decoder   r   rE   rF   r  rR  rS  rT  ru   r   suffixrJ  _IMAGE_EXT_BY_MIME_AUDIO_EXT_BY_MIMEr   )r%  ri   r>  r2  data_b64r   r   rR  rS  rT  rW  exts               r!   r:  r:  S  sU     {{6"Hx(
  JBD"&T$ZBF??8$@.224@C<-c377
 $//(3 .22fC *#s33(d339 	" OQTU$  < 1d;;<  KTSVWsY   C #+D% D 9D% D% D
*DD
D"D% !D""D% %	E.E

E)	thread_idmedia_filesforce_documentc          	       K   t         sddiS t        | j                  xs i j                  d      xs t	        j
                  d      t              }t	        j
                  d      }|sddiS dt         d| }d	|i}	d }
	 t        j                  d
      4 d {   }|r||d t         d}t               rd|d<   |j                  | d||	       d {   }|j                  dk7  r2dd|j                   d|j                  d d  icd d d       d {    S |j                         xs i }|j                  d      s(d|j                  d      xs dicd d d       d {    S |j                  d      }
dd l}|xs g D ]'  \  }}t#        j$                  t'        |            }|st(        j+                  d       =|j-                  |      \  }}|||rdndd}|r||d<   |j                  | d||	       d {   }|j                  dk7  r4dd|j                   d|j                  d d  ic cd d d       d {    S |j                         xs i }|j                  d      s*d|j                  d      xs dic cd d d       d {    S |j                  d      xs |
}
* d d d       d {    d|
dS 7 *7 7 7 j7 7 7 B7 # 1 d {  7  sw Y   +xY w# t.        $ r}dd | icY d }~S d }~ww xY ww)!NrN   r   r{   r|   r}   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*  r8  r   zsidecar returned r9  r   zsidecar reported failurer  r   z-[photon] standalone send skipping unsafe pathr/  r5  r.  r0  r0  Tr  zPhoton standalone send failed: )r'   r"   rZ   r[   r*   r+   r   r   r   r   rC  rv   r  r   r?   r   r2  r   r1  rJ   rE   rF   r3  r   )pconfigrh   r  r\  r]  r^  rz  tokenbaser   last_message_idr   	send_bodyr   r  r2  
media_pathr=  r4  r5  r6  att_bodyr   s                          r!   _standalone_sendrg    s     .//		"!!.1URYY?T5UD II,-E7
 	
 *+1TF3D'/G%)O2@$$T2 -	K -	Kf&#$8%89-	 %&*4Ih'#[[fENG )   ##s*#'89I9I8J"TYYW[X[_L]%^_-	K -	K -	K yy{(bxx~#TXXg%6%T:TU!-	K -	K -	K" #'((;"7
 (3(9r K$
H/LLSQ[_]	 NN#RS&11)<
&%'/G\,
 +2HZ(#[[f,-Hg )   ##s*#'89I9I8J"TYYW[X[_L]%^__S-	K -	K -	KT yy{(bxx~#TXXg%6%T:TUUY-	K -	K -	KZ #'((;"7"J?+K1-	K -	K^  ??_-	K-	K -	KJK-	K -	K -	K -	K -	K -	K`  @:1#>??@s3  A<L	?K, KK, 8KK3K	K, K	K, L	;KK, "K#K, 'L	(BK?K 5K5K, KK, L	=KK, KK, L	K.K, 9K:K, L	K, K	K, K, KK, K, K, K)K K)%K, ,	L5L;L<L	LL	c                    ddl m} | j                  ddd t        t        t
        ddgd|j                  t        d	t        d
dt        dddd       | j                  dd|j                  |j                         y)z.Called by the Hermes plugin loader at startup.r   )clirz   ziMessage via Photonc                    t        |       S r   )rx   rb   s    r!   <lambda>zregister.<locals>.<lambda>  s    M#$6 r/   rW   rY   zRun: hermes photon setup  (logs in via device flow, creates a Spectrum project, links your phone number, installs the spectrum-ts sidecar).re   PHOTON_ALLOWED_USERSPHOTON_ALLOW_ALL_USERSu   📱Tua  You are communicating via Photon Spectrum (iMessage). Treat replies like regular text messages — short and friendly. Markdown is rendered (bold, italics, lists, code), but keep formatting light and conversational. Recipient identifiers are E.164 phone numbers; never expose them in responses unless the user asked. Attachments arrive as metadata only.)ri   rA  adapter_factorycheck_fnr`   rc   required_envinstall_hintsetup_fnenv_enablement_fncron_deliver_env_varstandalone_sender_fnallowed_users_envallow_all_envmax_message_lengthrH  pii_safeallow_update_commandplatform_hintz1Set up and manage the Photon iMessage integration)ri   helprr  
handler_fnN)rD   ri  register_platformr0   r`   rc   gateway_setuprm   rg  rC  register_cli_commandregister_clidispatch)ctx_clis     r!   registerr    s     #6#'!)+BC$ ##)2-0.. !?;  $N @""==	  r/   )r   r   r    r   r\  r   r>  r:  )r]   r   r\  r\   )r\  zOptional[dict])r>  rJ   r\  r   )
r%  r[  ri   rJ   r>  rJ   r2  r\   r\  r<  )r`  r   rh   rJ   r  rJ   r\  r<  r]  zOptional[list]r^  r\   r\  r[  )GrB  
__future__r   r   rU  r   loggingr*   r   r   r(   r  rH   r  r  r   r   pathlibr   typingr   r   r	   r
   r   r   r'   ImportErrorgateway.configr   r   r  r   r   r   r   r   gateway.platforms.helpersr   authr   	getLoggerr?  rE   r   r   rC  r  r  __file__parentr,   rK   r  r  r   r"   r0   r7   rT   r`   rc   rm   rv   rx   r9  rX  rY  r:  rg  r  r.   r/   r!   <module>r     s  . #     	 	     
  '  ; ; O
 4  5 *			8	$
  # 
   ! H~$$y0  
    +" $8Ov &
 Y' Y~(
        44
4 4
 4 4B  $"& M@M@M@ M@
 M@  M@ M@ M@f2]5  s   D0 0	D<;D<