
    `gjY                        d Z ddlZddlZddlZddlZddlZddlZddlmZmZ ddl	m
Z
mZmZmZ 	 ddlZdZddlmZmZ ddlmZmZmZmZ  ej2                  e      Z G d	 d
e      ZdZdZdZ dZ!g dZ"dZ#dZ$de%dee%e%f   fdZ&de%de%de'fdZ(de)fdZ*de)fdZ+de)fdZ, G d de      Z-de.dz  fdZ/ddddde%de%d ee%   d!eee%      d"e)dee%e
f   fd#Z0d%d$Z1y# e$ r dZdZY w xY w)&ul  ntfy platform adapter (Hermes plugin).

Subscribes to a topic on ntfy.sh or any self-hosted ntfy server via
HTTP streaming (``/json`` endpoint with ``poll=false``) and publishes
replies via HTTP POST. No external SDK — only httpx, which is already
a Hermes dependency.

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

Configuration in config.yaml::

    platforms:
      ntfy:
        enabled: true
        extra:
          server: "https://ntfy.sh"       # or self-hosted URL
          topic: "hermes-in"              # subscribe topic (incoming)
          publish_topic: "hermes-out"     # optional — defaults to topic
          token: "..."                    # optional Bearer / Basic auth token
          markdown: true                  # optional — enable markdown (default: false)

Environment variables (all read at adapter construct time, env wins over
config.yaml ``extra``):

    NTFY_TOPIC                 Topic to subscribe to (required)
    NTFY_SERVER_URL            Server URL (default: https://ntfy.sh)
    NTFY_TOKEN                 Bearer token or 'user:pass' for Basic auth
    NTFY_PUBLISH_TOPIC         Reply topic (defaults to NTFY_TOPIC)
    NTFY_MARKDOWN              "true"/"1"/"yes" enables X-Markdown header
    NTFY_ALLOWED_USERS         Allowlist (treated by gateway as user IDs;
                               on ntfy these are topic names)
    NTFY_ALLOW_ALL_USERS       Allow any topic — dev only
    NTFY_HOME_CHANNEL          Default topic for cron / notification delivery
    NTFY_HOME_CHANNEL_NAME     Human label for the home channel

Identity model: ntfy has no native authenticated user identity. The
``title`` field is publisher-controlled and is NOT used for
authorization. Each topic is treated as a single trusted channel —
``user_id`` is fixed to the topic name. Use a private topic protected
by a read token for any real trust boundary.
    N)datetimetimezone)AnyDictListOptionalTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResultc                       e Zd ZdZy)_FatalStreamErrorz<Raised when a stream error is unrecoverable (e.g. 401, 404).N)__name__
__module____qualname____doc__     Q/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/ntfy/adapter.pyr   r   J   s    Fr   r   zhttps://ntfy.shi   ,  i  )      
      <   Z   zhermes-agenttokenreturnc                     | si S | j                         } | si S d| v r8ddl}|j                  | j                               j	                         }dd| iS dd|  iS )u  Build an ``Authorization`` header from an ntfy token.

    Shared by :class:`NtfyAdapter._auth_headers` and :func:`_standalone_send`
    so both paths follow the same auth shape and whitespace-stripping rules.

    Tokens are stripped of surrounding whitespace — pasted tokens often
    carry trailing newlines that would otherwise render the header
    malformed (``Authorization: Bearer foo\n``).  ``user:pass`` tokens
    become Basic auth; anything else is treated as a Bearer token.
    Returns ``{}`` when no token is configured.
    :r   NAuthorizationzBasic zBearer )stripbase64	b64encodeencodedecode)r   r%   encodeds      r   _build_auth_headerr*   W   sl     	KKME	
e|""5<<>299;6'!344wug.//r   messagecontextc                    t        |       t        kD  r%t        j                  d|t        |       t               | dt         j	                  d      S )zApply the ntfy 4096-char limit, logging a warning on truncation.

    ``context`` is included in the log message so adapter and standalone
    truncations can be told apart in logs.
    z7%s: truncating message from %d to %d chars (ntfy limit)Nutf-8)lenMAX_MESSAGE_LENGTHloggerwarningr'   )r+   r,   s     r   _truncate_bodyr3   o   sH     7|((ES\#5	
 &&'..w77r   c                  n    t         syt        j                  dd      j                         } t	        |       S )zCheck whether the ntfy adapter is installable and minimally configured.

    Reads ``NTFY_TOPIC`` directly to avoid the cost of a full
    ``load_gateway_config()`` (which also writes to ``os.environ``) on
    every pre-flight check.
    F
NTFY_TOPIC )HTTPX_AVAILABLEosgetenvr$   bool)topics    r   check_requirementsr<   }   s-     IIlB'--/E;r   c                     t        | di       xs i }|j                  d      xs t        j                  dd      }t	        |      S )z;Validate that the configured ntfy platform has a topic set.extrar;   r5   r6   )getattrgetr8   r9   r:   configr>   r;   s      r   validate_configrC      s=    FGR(.BEIIg="))L""=E;r   c                     t        | di       xs i }t        j                  d      xs |j                  dd      }t	        |      S )z6Check whether ntfy is configured (env or config.yaml).r>   r5   r;   r6   )r?   r8   r9   r@   r:   rA   s      r   is_connectedrE      s=    FGR(.BEIIl#=uyy"'=E;r   c                       e Zd ZdZeZdef fdZdddedefdZdd
Z	de
dee
e
f   dd	fdZddZdee
ef   dd	fdZde
defdZ	 	 dde
de
dee
   deee
ef      def
dZdde
dd	fdZde
dee
ef   fdZdee
e
f   fdZ xZS )NtfyAdapteru   ntfy adapter.

    Subscribes to a topic via HTTP streaming (``/json`` endpoint) and
    publishes replies via HTTP POST. No external SDK — only httpx.
    rB   c                 :   t        d      }t        | 	  ||       |j                  xs i }|j	                  d      xs t        j                  dt              j                  d      | _	        |j	                  d      xs t        j                  dd      | _
        |j	                  d	      xs$ t        j                  d
d      xs | j                  | _        |j	                  d      xs t        j                  dd      | _        d | _        d | _        i | _        y )Nntfy)rB   platformserverNTFY_SERVER_URL/r;   r5   r6   publish_topicNTFY_PUBLISH_TOPICr   
NTFY_TOKEN)r	   super__init__r>   r@   r8   r9   DEFAULT_SERVERrstrip_server_topic_publish_topic_token_stream_task_http_client_seen_messages)selfrB   rJ   r>   	__class__s       r   rR   zNtfyAdapter.__init__   s    F#:"IIh <yy*N;
&+ 	 !99W-L<1LIIo& yy-r2{{ 	
 !99W-L<1L48;? 13r   F)is_reconnectr^   r    c                6  K   t         s!t        j                  d| j                         y| j                  s!t        j                  d| j                         y	 t        j                  d      | _        t        j                  | j                               | _        | j                          t        j                  d| j                  | j                  | j                         y# t        $ r+}t        j!                  d| j                  |       Y d}~yd}~ww xY ww)	z<Connect to ntfy by starting the streaming subscription task.z0[%s] httpx not installed. Run: pip install httpxFz[%s] NTFY_TOPIC not configuredNtimeoutu'   [%s] Connected — subscribing to %s/%sTz[%s] Failed to connect: %s)r7   r1   r2   namerV   httpxAsyncClientrZ   asynciocreate_task_run_streamrY   _mark_connectedinforU   	Exceptionerror)r\   r^   es      r   connectzNtfyAdapter.connect   s     NNMtyyY{{NN;TYYG	 % 1 1$ ?D ' 3 3D4D4D4F GD  "KKA499dll\`\g\gh 	LL5tyy!D	s1   ADB	C" !D"	D+!DDDDNc                 B  K   d}d}| j                    d| j                   d}| j                         }| j                  r	 t        j                  d| j                  |       t        j                         }| j                  ||       d{    | j                  syt        j                         |z
  d	k\  rd}t        t!        |t#        t              d
z
           }t        j%                  d| j                  |       t        j&                  |       d{    |d
z  }| j                  ryy7 # t        j                  $ r Y yt        $ r
 d| _        Y yt        $ r=}| j                  sY d}~yt        j                  d| j                  |       Y d}~d}~ww xY w7 w)z8Subscribe to the ntfy topic with automatic reconnection.r   g        rM   z/jsonz[%s] Opening stream to %sNFz[%s] Stream error: %sg      N@   z[%s] Reconnecting in %ds...)rU   rV   _auth_headers_runningr1   debugrb   time	monotonic_consume_streamre   CancelledErrorr   rj   r2   RECONNECT_BACKOFFminr/   ri   sleep)r\   backoff_idxstream_starturlheadersrl   delays          r   rg   zNtfyAdapter._run_stream   s[    !a}E2$$&mmF8$))SI#~~/**3888 == ~~,.$6%c+s;L7MPQ7Q&RSEKK5tyy%H--&&&1K1 mm 9)) $  % F}}6		1EEF 'sy   =F A
D/ 
D-D/ BFFF+F-D/ /FFFFFF)F.!FFFFr|   r}   c                   K   ddi}| j                   j                  d|||t        j                  dt        dd            4 d{   }|j
                  dk(  r?t        j                  d	| j                         | j                  d
dd       t        d      |j
                  dk(  rXt        j                  d| j                  | j                         | j                  dd| j                   dd       t        d      |j                          |j                         2 3 d{   }| j                  s ddd      d{    y|j                         }|s:	 t!        j"                  |      }|j'                  d      dk(  se| j)                  |       d{    7 Y7 }7 b# t         j$                  $ r Y w xY w7 %6 ddd      d{  7   y# 1 d{  7  sw Y   yxY ww)z6Open an HTTP streaming connection and dispatch events.pollfalseGET      .@)rm   readwritepool)r}   paramsra   Ni  uO   [%s] Authentication failed (401) — stopping reconnect loop. Check NTFY_TOKEN.ntfy_unauthorizedz2ntfy server rejected auth (401). Check NTFY_TOKEN.F)	retryablez401 Unauthorizedi  u;   [%s] Topic not found (404): %s — stopping reconnect loop.ntfy_topic_not_foundzntfy topic 'z!' returned 404. Check NTFY_TOPIC.z404 Not Foundeventr+   )rZ   streamrc   TimeoutSTREAM_TIMEOUT_SECONDSstatus_coder1   rk   rb   _set_fatal_errorr   rV   raise_for_statusaiter_linesrq   r$   jsonloadsJSONDecodeErrorr@   _on_message)r\   r|   r}   r   responseliner   s          r   ru   zNtfyAdapter._consume_stream   s     '"$$++MM$5KSW^bc , 
 *	2 *	2 ##s*eII %%'H# & 
 ((:;;##s*QIIt{{ %%*"4;;-/PQ# & 
 (88%%'&224 2 2d}}C*	2 *	2 *	2D zz| JJt,E 99W%2**5111U*	2>2?*	2N ++  2 5?*	2 *	2 *	2 *	2 *	2s   AG)FG)
CG G$F"%G(G6G)F$G)GF&0GGF?GG)"G$G)&F<9G;F<<GGG)GG)G&GG&"G)c                   K   d| _         | j                          | j                  r6| j                  j                          	 | j                   d{    d| _        | j                  r)| j                  j                          d{    d| _        | j                  j                          t        j                  d| j                         y7 {# t        j
                  $ r Y w xY w7 aw)zDisconnect from ntfy.FNz[%s] Disconnected)rq   _mark_disconnectedrY   cancelre   rv   rZ   acloser[   clearr1   ri   rb   r\   s    r   
disconnectzNtfyAdapter.disconnect  s     !$$&'''' !%D##**,,, $D!!#'3 ()) 
 -sH   >C)C CC 0C)C'AC)C C$!C)#C$$C)r   c                 T  K   |j                  d      xs t        j                         j                  }| j	                  |      r"t
        j                  d| j                  |       y|j                  d      xs g }t        |v r!t
        j                  d| j                         y|j                  d      xs dj                         }|s!t
        j                  d| j                         y|j                  d	      xs | j                  }|}|}| j                  ||d
||      }|j                  d      }		 |	r.t        j                  t        |	      t        j                         n#t        j"                  t        j                         }
t+        |t,        j.                  ||||
      }t
        j                  d| j                  ||dd        | j1                  |       d{    y# t$        t&        t(        f$ r' t        j"                  t        j                         }
Y w xY w7 Cw)z'Process an incoming ntfy message event.idz#[%s] Duplicate message %s, skippingNtagsz$[%s] Skipping own message (echo tag)r+   r6   z![%s] Empty message body, skippingr;   dm)chat_id	chat_name	chat_typeuser_id	user_namers   )tz)textmessage_typesource
message_idraw_message	timestampz[%s] Message on topic %s: %sP   )r@   uuiduuid4hex_is_duplicater1   rr   rb   	_ECHO_TAGr$   rV   build_sourcer   fromtimestampintr   utcnow
ValueErrorOSError	TypeErrorr   r   TEXThandle_message)r\   r   msg_idr   r   r;   r   r   r   unix_tsr   message_events               r   r   zNtfyAdapter._on_message4  s    44DJJL$4$4f%LL>		6R yy &BLL?K		)$*113LL<diiH		'"1dkk 	"" # 
 ))F#	6  &&s7|E ( =  %$))
 	3TYYtCRyQ!!-000 GY/ 	6 5I	6 	1s8   D3H(6AG( 
AH("H&#H((8H# H("H##H(r   c                 0   t        j                          }t        | j                        t        kD  rB|t        z
  }| j                  j                         D ci c]  \  }}||kD  s|| c}}| _        || j                  v ry|| j                  |<   yc c}}w )zHReturn True if this message ID was already seen within the dedup window.TF)rs   r/   r[   DEDUP_MAX_SIZEDEDUP_WINDOW_SECONDSitems)r\   r   r   cutoffkvs         r   r   zNtfyAdapter._is_duplicateo  s    iikt""#n4//F484G4G4M4M4O"^DAqSTW]S]1a4"^DT(((&)F# #_s   B%Br   contentreply_tometadatac           	      &  K   |xs i }|j                  d      xs | j                  xs |}| j                  st        dd      S | j                   d| }| j
                  j                  xs i j                  dd      }i | j                         dt        d}|rd	|d
<   t        |      | j                  kD  r5t        j                  d| j                  t        |      | j                         |d| j                   }		 | j                  j                  ||	j                  d      |d       d{   }
|
j                   dk  rR	 |
j#                         }|j                  d      xs! t%        j&                         j(                  dd }t        d|      S |
j,                  }t        j                  d| j                  |
j                   |dd        t        dd|
j                    d|dd        S 7 # t*        $ r$ t%        j&                         j(                  dd }Y w xY w# t.        j0                  $ r t        dd      cY S t*        $ rA}t        j3                  d| j                  |       t        dt5        |            cY d}~S d}~ww xY ww)z2Publish a message to the configured publish topic.rN   FzHTTP client not initialized)successrk   rM   markdowntext/plain; charset=utf-8zContent-TypezX-Tagstrue
X-Markdownz7[%s] Message truncated from %d to %d chars (ntfy limit)Nr.   r   )r   r}   ra   r   r      T)r   r   z[%s] Send failed HTTP %d: %s   zHTTP : zTimeout publishing to ntfyz[%s] Send error: %s)r@   rW   rZ   r   rU   rB   r>   rp   r   r/   r0   r1   r2   rb   postr'   r   r   r   r   r   rj   r   rc   TimeoutExceptionrk   str)r\   r   r   r   r   rN   r|   markdown_enabledr}   bodyrespdatareturned_id	body_textrl   s                  r   sendzNtfyAdapter.send}  s`     >r _5W9L9LWPW  e3PQQa/ KK--388UK
  "
7

 $*GL!w<$111NNI		3w<)@)@ ///0	;**//T[[17D 0  D #%899;D"&((4."IDJJL4D4DSb4IK "$;GG		INN9499dFVFVXabfcfXgheU4;K;K:LByY]Z]N_3`aa ! 8"&**,"2"23B"7K8 %% 	Qe3OPP 	;LL.		1=e3q6::	;s   C<J?1H# 0G11H# AG3 	H# JAH# 0J1H# 3*H H# H  H# # JJJ6J	JJ	JJc                    K   yw)z(ntfy does not support typing indicators.Nr   )r\   r   r   s      r   send_typingzNtfyAdapter.send_typing  s	     s   c                    K   |ddS w)z&Return basic info about an ntfy topic.r   )rb   typer   )r\   r   s     r   get_chat_infozNtfyAdapter.get_chat_info  s     ..s   c                 ,    t        | j                        S )z4Build Authorization header if a token is configured.)r*   rX   r   s    r   rp   zNtfyAdapter._auth_headers  s    !$++..r   r    N)NNN)r   r   r   r   r0   r
   rR   r:   rm   rg   r   r   ru   r   r   r   r   r   r   r   r   r   rp   __classcell__)r]   s   @r   rG   rG      s"    ,3~ 32 5: T d &B.2 .2tCH~ .2$ .2`4,71tCH~ 71$ 71v
C 
D 
$ #'-11;1; 1; 3-	1;
 4S>*1; 
1;f  /3 /4S> //tCH~ /r   rG   c                  Z   t        j                  dd      j                         } | sy| t        j                  dt              j	                  d      d}t        j                  dd      j                         }|r||d<   t        j                  d	d      j                         }|r||d
<   t        j                  dd      j                         j                         }|r|dv |d<   t        j                  dd      j                         xs | }|r|t        j                  d|      d|d<   |S )u]  Seed ``PlatformConfig.extra`` from env vars during gateway config load.

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

    The special ``home_channel`` key in the returned dict is handled by the
    core hook — it becomes a proper ``HomeChannel`` dataclass on the
    ``PlatformConfig`` rather than being merged into ``extra``.
    r5   r6   NrL   rM   )r;   rK   rO   rN   rP   r   NTFY_MARKDOWN1r   yesr   NTFY_HOME_CHANNELNTFY_HOME_CHANNEL_NAME)r   rb   home_channel)r8   r9   r$   rS   rT   lower)r;   seedrN   r   r   homes         r   _env_enablementr     s    IIlB'--/E))-~>EEcJD II2B7==?M -_IIlB'--/EWyy"-335;;=H#';;Z99("-335>DII6= 
^ Kr   )	thread_idmedia_filesforce_documentr   r   r   r   c                `  K   t         sddiS t        | di       xs i }|j                  d      xs t        j                  dt
              j                  d      }|xsp |j                  d      xs] t        j                  dd	      j                         xs7 |j                  d
      xs$ t        j                  dd	      j                         }|sddiS |j                  d      xs t        j                  dd	      }	t        j                  dd	      j                         j                         }
t        |j                  d            xs |
dv }dt        dt        |	      }|rd|d<   t        |d      }| d| }	 t        j                  d      4 d{   }|j                  |||       d{   }ddd      d{    j                   dk\  r!dd|j                    d|j"                  dd  iS 	 |j%                         }|j                  d       xs! t'        j(                         j*                  dd! }d"d#||d$S 7 7 7 # 1 d{  7  sw Y   xY w# t,        $ r$ t'        j(                         j*                  dd! }Y Nw xY w# t,        $ r}dd%| icY d}~S d}~ww xY ww)&u9  Out-of-process publish for cron / send_message_tool fallbacks.

    Used by ``tools/send_message_tool._send_via_adapter`` and the cron
    scheduler when the gateway runner is not in this process (e.g.
    ``hermes cron`` running standalone). Without this hook,
    ``deliver=ntfy`` cron jobs fail with ``No live adapter for platform``.

    ``thread_id`` and ``media_files`` are accepted for signature parity
    only — ntfy has no thread or attachment primitive. Markdown is
    honored if ``NTFY_MARKDOWN`` is set OR ``pconfig.extra["markdown"]``
    is True.
    rk   z)ntfy standalone send: httpx not installedr>   rK   rL   rM   rN   rO   r6   r;   r5   z/ntfy standalone send: NTFY_TOPIC not configuredr   rP   r   r   r   r   r   r   r   zntfy standalone)r,   r   r`   N)r   r}   r   z
ntfy HTTP r   r   r   r   TrI   )r   rJ   r   r   zntfy standalone send failed: )r7   r?   r@   r8   r9   rS   rT   r$   r   r:   r   r*   r3   rc   rd   r   r   r   r   r   r   r   rj   )pconfigr   r+   r   r   r   r>   rK   rN   r   markdown_envr   r}   r   r|   clientr   r   r   rl   s                       r   _standalone_sendr     s    * DEEGWb)/RE		( 	899&7fSk 
 	 	/99_%	/99)2.446	/ 99W	/ 99\2&,,.  JKKIIg="))L""=E99_b1779??ALEIIj12ZlFZ6Z:imSefkSlmG &'+<=DHAm_
%C>$$T2 	I 	IfS$HHD	I 	Is"z$*:*:);2diio=NOPP	+99;DXXd^<tzz|'7'7'<F  V]cdd	IH	I 	I 	I 	I  	+ZZ\%%cr*F	+  >8<==>s   E<J.?J IJ I5I6I:J I
3J 9J.;AI! ?J J.J I
J IIIJ !*JJ JJ 	J+J& J+!J.&J++J.c                 ~    | j                  ddd t        t        t        dgdt        dt
        ddt        dd	d	d
       y)uE   Plugin entry point — called by the Hermes plugin system at startup.rI   c                     t        |       S r   )rG   )cfgs    r   <lambda>zregister.<locals>.<lambda>0  s    K$4 r   r5   z1pip install httpx   # already a Hermes dependencyr   NTFY_ALLOWED_USERSNTFY_ALLOW_ALL_USERSu   🔔Tu  You are communicating via ntfy push notifications. Use plain text by default — ntfy supports optional markdown (set markdown: true in config or NTFY_MARKDOWN=true). Keep responses concise; ntfy is a push notification service with a 4096-character per-message limit.)rb   labeladapter_factorycheck_fnrC   rE   required_envinstall_hintenv_enablement_fncron_deliver_env_varstandalone_sender_fnallowed_users_envallow_all_envmax_message_lengthemojipii_safeallow_update_commandplatform_hintN)register_platformr<   rC   rE   r   r   r0   )ctxs    r   registerr  +  sW    4#'!"^H * 1 ..,- !7=  $r   r   )2r   re   r   loggingr8   rs   r   r   r   typingr   r   r   r   rc   r7   ImportErrorgateway.configr	   r
   gateway.platforms.baser   r   r   r   	getLoggerr   r1   rj   r   rS   r0   r   r   rw   r   r   r   r*   bytesr3   r:   r<   rC   rE   rG   dictr   r   r  r   r   r   <module>r     s  ,\    	   ' , ,O
 4  
		8	$G	 G #  &  	0c 0d38n 008C 8S 8U 8
D 
t D d/% d/X	# #V  $'+ >>>> >>
 }>> $s)$>> >> 
#s(^>>B&a  OEs   C 	C('C(