
    jW                        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n# e$ r dZdZY nw xY wddlmZmZ ddlmZmZmZmZ  ej        e          Z G d	 d
e          ZdZdZdZ dZ!g 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"Z/d$d#Z0dS )%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dS )_FatalStreamErrorz<Raised when a stream error is unrecoverable (e.g. 401, 404).N)__name__
__module____qualname____doc__     =/usr/local/lib/hermes-agent/plugins/platforms/ntfy/adapter.pyr   r   J   s        FFFFr   r   zhttps://ntfy.shi   ,  i  )      
      <   Z   tokenreturnc                     | si S |                                  } | si S d| v rDddl}|                    |                                                                           }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*   V   s      	KKMME 	
e||""5<<>>2299;;!3'!3!344.u..//r   messagecontextc                    t          |           t          k    r/t                              d|t          |           t                     | dt                                       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   n   s_     7||(((ES\\#5	
 	
 	
 &&&'..w777r   c                      t           sdS t          j        dd                                          } 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:      uIlB''--//E;;r   c                     t          | di           pi }|                    d          p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      sF    FGR((.BEIIg=")L""="=E;;r   c                     t          | di           pi }t          j        d          p|                    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      sF    FGR((.BEIl##=uyy"'='=E;;r   c                   4    e Zd ZdZeZdef fdZ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        pi }|                    d          pt          j        dt                                        d          | _	        |                    d          pt          j        dd          | _
        |                    d	          pt          j        d
d          p| j
        | _        |                    d          pt          j        dd          | _        d | _        d | _        i | _        d S )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 <y*N;;
&++ 	 !99W--L<1L1LIIo&& y-r22{ 	
 !99W--L<1L1L48;? 13r   r    c                 ,  K   t           s"t                              d| j                   dS | j        s"t                              d| j                   dS 	 t          j        d          | _        t          j	        | 
                                          | _        |                                  t                              d| j        | j        | j                   dS # t          $ r,}t                              d| j        |           Y d}~dS d}~ww xY 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\   es     r   connectzNtfyAdapter.connect   s      	NNMtyYYY5{ 	NN;TYGGG5	 % 1$ ? ? ?D ' 3D4D4D4F4F G GD  """KKA49dl\`\ghhh4 	 	 	LL5ty!DDD55555	s   BC 
D'!DDNc                 "  K   d}d}| j          d| j         d}|                                 }| j        rZ	 t                              d| j        |           t          j                    }| 	                    ||           d{V  ni# t          j        $ r Y dS t          $ r d| _        Y dS t          $ r8}| j        sY d}~dS t                              d| j        |           Y d}~nd}~ww xY w| j        sdS t          j                    |z
  d	k    rd}t          t!          |t#          t                    d
z
                     }t                              d| j        |           t          j        |           d{V  |d
z  }| j        XdS dS )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   debugra   time	monotonic_consume_streamrd   CancelledErrorr   ri   r2   RECONNECT_BACKOFFminr/   rh   sleep)r\   backoff_idxstream_starturlheadersrk   delays          r   rf   zNtfyAdapter._run_stream   s     !22222$$&&m 	F8$)SIII#~//**38888888888)   $    % F F F} FFFFF6	1EEEEEEEEF
 =  ~,.$66%c+s;L7M7MPQ7Q&R&RSEKK5ty%HHH-&&&&&&&&&1K1 m 	 	 	 	 	s*   AB C-C-,	C-5C(!C((C-r{   r|   c                   K   ddi}| j                             d|||t          j        dt          dd                    4 d{V 	 }|j        dk    rGt                              d	| j                   | 	                    d
dd           t          d          |j        dk    rVt                              d| j        | j                   | 	                    dd| j         dd           t          d          |                                 |                                2 3 d{V }| j        s ddd          d{V  dS |                                }|s9	 t!          j        |          }n# t           j        $ r Y `w xY w|                    d          dk    r|                     |           d{V  6 	 ddd          d{V  dS # 1 d{V swxY w Y   dS )z6Open an HTTP streaming connection and dispatch events.pollfalseGET      .@)rl   readwritepool)r|   paramsr`   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   streamrb   TimeoutSTREAM_TIMEOUT_SECONDSstatus_coder1   rj   ra   _set_fatal_errorr   rV   raise_for_statusaiter_linesrp   r$   jsonloadsJSONDecodeErrorr@   _on_message)r\   r{   r|   r   responseliner   s          r   rt   zNtfyAdapter._consume_stream   s&      '"$++M$5KSW^bccc , 
 
 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 #s**eI   %%'H# &   
 ((:;;;#s**QIt{   %%*Q4;QQQ# &   
 (888%%'''&2244 2 2 2 2 2 2 2d} C*	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2D zz||  Jt,,EE+   H99W%%22**5111111111 54?*	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2 *	2sI   CG$F=*	GGE32G3FGF9G
GGc                   K   d| _         |                                  | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        | j        r&| j                                         d{V  d| _        | j        	                                 t                              d| j                   dS )zDisconnect from ntfy.FNz[%s] Disconnected)rp   _mark_disconnectedrY   cancelrd   ru   rZ   acloser[   clearr1   rh   ra   r\   s    r   
disconnectzNtfyAdapter.disconnect  s      !!! 	%$$&&&''''''''')    $D 	%#**,,,,,,,,, $D!!###'33333s   A AAr   c                   K   |                     d          pt          j                    j        }|                     |          r#t
                              d| j        |           dS |                     d          pd                                }|s"t
                              d| j                   dS |                     d          p| j	        }|}|}| 
                    ||d||	          }|                     d
          }	 |r-t          j        t          |          t          j                  nt          j        t          j                  }	n<# t"          t$          t&          f$ r" t          j        t          j                  }	Y nw xY wt)          |t*          j        ||||	          }
t
                              d| j        ||dd                    |                     |
           d{V  dS )z'Process an incoming ntfy message event.idz#[%s] Duplicate message %s, skippingNr+   r6   z![%s] Empty message body, skippingr;   dm)chat_id	chat_name	chat_typeuser_id	user_namerr   )tz)textmessage_typesource
message_idraw_message	timestampz[%s] Message on topic %s: %sP   )r@   uuiduuid4hex_is_duplicater1   rq   ra   r$   rV   build_sourcer   fromtimestampintr   utcnow
ValueErrorOSError	TypeErrorr   r   TEXThandle_message)r\   r   msg_idr   r;   r   r   r   unix_tsr   message_events              r   r   zNtfyAdapter._on_message3  s     44DJLL$4f%% 	LL>	6RRRF		)$$*1133 	LL<diHHHF		'""1dk 	"" # 
 
 ))F##	6 >&s7||EEEE ( = = = I GY/ 	6 	6 	6 555III	6 %$)
 
 
 	3TYtCRCyQQQ!!-00000000000s   AE 6FFr   c                     t          j                     }t          | j                  t          k    r4|t          z
  fd| j                                        D             | _        || j        v rdS || j        |<   dS )zHReturn True if this message ID was already seen within the dedup window.c                 (    i | ]\  }}|k    ||S r   r   ).0kvcutoffs      r   
<dictcomp>z-NtfyAdapter._is_duplicate.<locals>.<dictcomp>m  s+    "^"^"^DAqSTW]S]S]1aS]S]S]r   TF)rr   r/   r[   DEDUP_MAX_SIZEDEDUP_WINDOW_SECONDSitems)r\   r   r   r   s      @r   r   zNtfyAdapter._is_duplicateh  s    ikkt"##n44//F"^"^"^"^D4G4M4M4O4O"^"^"^DT(((4&)F#ur   r   contentreply_tometadatac           	        K   |pi }|                     d          p| j        p|}| j        st          dd          S | j         d| }| j        j        pi                      dd          }i |                                 ddi}|rd	|d
<   t          |          | j	        k    r4t                              d| j        t          |          | j	                   |d| j	                 }		 | j                            ||	                    d          |d           d{V }
|
j        dk     r	 |
                                }|                     d          pt#          j                    j        dd         }n0# t(          $ r# t#          j                    j        dd         }Y nw xY wt          d|          S |
j        }t                              d| j        |
j        |dd                    t          dd|
j         d|dd                    S # t,          j        $ r t          dd          cY S t(          $ rI}t                              d| j        |           t          dt3          |                    cY d}~S d}~ww xY w)z2Publish a message to the configured publish topic.rN   FzHTTP client not initialized)successrj   rM   markdownContent-Typetext/plain; charset=utf-8true
X-Markdownz7[%s] Message truncated from %d to %d chars (ntfy limit)Nr.   r   )r   r|   r`   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>   ro   r/   r0   r1   r2   ra   postr'   r   r   r   r   r   ri   r   rb   TimeoutExceptionrj   str)r\   r   r   r   r   rN   r{   markdown_enabledr|   bodyrespdatareturned_id	body_textrk   s                  r   sendzNtfyAdapter.sendv  s      >r _55W9LWPW  	Re3PQQQQ//// K-388UKKWT''))W>;VWW 	+$*GL!w<<$111NNI	3w<<)@   ///0	;*//T[[117D 0        D #%%899;;D"&((4.."IDJLL4DSbS4IKK  8 8 8"&*,,"23B3"7KKK8!$;GGGG	INN949dFVXabfcfbfXghhhe3`4;K3`3`yY]Z]Y]3`3`aaaa% 	Q 	Q 	Qe3OPPPPPP 	; 	; 	;LL.	1===e3q66:::::::::	;sQ   (AH +A	E5 4H 5*F"H !F""H 6AH  J2	J;>I?9J?Jc                 
   K   dS )z(ntfy does not support typing indicators.Nr   )r\   r   r   s      r   send_typingzNtfyAdapter.send_typing  s      r   c                    K   |ddS )z&Return basic info about an ntfy topic.r   )ra   typer   )r\   r   s     r   get_chat_infozNtfyAdapter.get_chat_info  s      ...r   c                 *    t          | j                  S )z4Build Authorization header if a token is configured.)r*   rX   r   s    r   ro   zNtfyAdapter._auth_headers  s    !$+...r   r    N)NNN)r   r   r   r   r0   r
   rR   r:   rl   rf   r   r   rt   r   r   r   r   r   r   r   r   r   ro   __classcell__)r]   s   @r   rG   rG      s         ,3~ 3 3 3 3 3 32t    &   B.2 .2tCH~ .2$ .2 .2 .2 .2`4 4 4 4,11tCH~ 11$ 11 11 11 11j
C 
D 
 
 
 
$ #'-1-; -;-; -; 3-	-;
 4S>*-; 
-; -; -; -;^      /3 /4S> / / / //tCH~ / / / / / / / /r   rG   c                     t          j        dd                                          } | sdS | t          j        dt                                        d          d}t          j        dd                                          }|r||d<   t          j        d	d                                          }|r||d
<   t          j        dd                                                                          }|r|dv |d<   t          j        dd                                          p| }|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   ra   home_channel)r8   r9   r$   rS   rT   lower)r;   seedrN   r   r   homes         r   _env_enablementr     sR    IlB''--//E t)-~>>EEcJJ D I2B77==??M . -_IlB''--//E Wy"--3355;;==H <#';;Z9("--3355>D 
I6== 
  
^ Kr   )	thread_idmedia_filesforce_documentr   r   r   r   c                  K   t           sddiS t          | di           pi }|                    d          pt          j        dt
                                        d          }|pw|                    d          pbt          j        dd	                                          p;|                    d
          p&t          j        dd	                                          }|sddiS |                    d          pt          j        dd	          }	t          j        dd	                                                                          }
t          |                    d                    p|
dv }ddit          |	          }|rd|d<   t          |d          }| d| }	 t          j        d          4 d{V }|                    |||           d{V }ddd          d{V  n# 1 d{V swxY w Y   |j        dk    rdd|j         d|j        dd          iS 	 |                                }|                    d           pt%          j                    j        dd!         }n0# t*          $ r# t%          j                    j        dd!         }Y nw xY wd"d#||d$S # t*          $ r}dd%| icY d}~S d}~ww xY 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.
    rj   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*   r3   rb   rc   r   r   r   r   r   r   r   ri   )pconfigr   r+   r   r   r   r>   rK   rN   r   markdown_envr   r|   r   r{   clientr   r   r   rk   s                       r   _standalone_sendr     s     *  FDEEGWb))/RE		( 	89&77fSkk 
 	 	/99_%%	/9)2..4466	/ 99W	/ 9\2&&,,..   LJKKIIg=")L""="=E9_b117799??AALEIIj1122ZlFZ6Z:X>PQV>W>WXG ' &'+<===D
%
%m
%
%C>$T222 	I 	I 	I 	I 	I 	I 	IfS$HHHHHHHHD	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	I 	Is""O$*:OOdioOOPP	+99;;DXXd^^<tz||'7'<FF 	+ 	+ 	+Z\\%crc*FFF	+V]cddd > > ><<<=======>sg   J% :G+J% +
G55J% 8G59)J% $A	I. -J% .*JJ% J	J% %
K /J;5K ;K c                     |                      ddd t          t          t          dgdt          dt
          ddt          dd	d	d
           dS )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>%  s    K$4$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.)ra   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     sn    44#'!"^H * 1 ..,- !7=  $ $ $ $ $r   r   )1r   rd   r   loggingr8   rr   r   r   r   typingr   r   r   r   rb   r7   ImportErrorgateway.configr	   r
   gateway.platforms.baser   r   r   r   	getLoggerr   r1   ri   r   rS   r0   r   r   rv   r   r   r*   bytesr3   r:   r<   rC   rE   rG   dictr   r   r  r   r   r   <module>r     s  , ,\    				   ' ' ' ' ' ' ' ' , , , , , , , , , , , ,LLLOO   OEEE 4 3 3 3 3 3 3 3            
	8	$	$G G G G G	 G G G #  &&&  0c 0d38n 0 0 0 008C 8S 8U 8 8 8 8
D 
 
 
 
t    D    Z/ Z/ Z/ Z/ Z/% Z/ Z/ Z/D	# # # # #V  $'+ >> >> >>>> >>
 }>> $s)$>> >> 
#s(^>> >> >> >>B& & & & & &s   7 	AA