
    `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mZ ddl	m
Z
mZ ddlmZ  ej                  e      Z eddh      Zdd	Z G d
 d      Zy)u-  Persistent registry of delivery targets that are confirmed unreachable.

When a messaging platform reports that a target chat is permanently gone — a
deleted group (``Forbidden: the group chat was deleted``), a bot kicked/blocked,
or a deactivated user — re-sending to it on every cron tick or every fan-out
delivery wastes a send attempt against the platform's flood-control envelope and
spams the logs.  This registry lets the delivery layer short-circuit a target it
has already proven dead, while staying self-healing: any successful send to that
target clears the flag, so a user who re-adds the bot (or restores the chat)
recovers automatically with no manual cleanup.

Scope is deliberately narrow.  Only *whole-chat* deaths are recorded — the
``forbidden`` and chat-level ``not_found`` (``chat not found``) error kinds.
Thread/topic-level ``not_found`` is NOT recorded here: the adapters already
self-heal that by retrying without ``reply_to`` (see the Telegram adapter's
reply-target-deleted path), and a deleted topic does not mean the parent chat is
dead.

The store is a small JSON file under the active profile's HERMES_HOME so each
profile keeps its own dead set.  Reads/writes are best-effort: a corrupt or
unwritable file degrades to an in-memory-only registry rather than raising on
the delivery path.
    )annotationsN)Path)DictOptional)get_hermes_home	forbidden	not_foundc                    t        |       j                         j                          dt        |      j                          S )z-Canonical key for a (platform, chat_id) pair.:)strstriplower)platformchat_ids     G/root/.hermes/venv/lib/python3.12/site-packages/gateway/dead_targets.py
_normalizer   +   s8    (m!!#))+,Ac'l.@.@.B-CDD    c                  f    e Zd ZdZdddZddZddZedd       ZddZ		 d	 	 	 ddZ
dd	Zdd
Zy)DeadTargetRegistryzThread-safe, persistent set of confirmed-dead delivery targets.

    Keyed on ``platform:chat_id``.  Stores the reason and a timestamp for
    observability.  Self-healing: :meth:`clear` (called on a successful send)
    removes the flag.
    Nc                    t        j                         | _        i | _        ||| _        nt               dz  dz  | _        | j                          y )Ngatewayzdead_targets.json)	threadingRLock_lock_dead_pathr   _load)selfpaths     r   __init__zDeadTargetRegistry.__init__8   sB    __&
35
DJ(*Y69LLDJ

r   c                   	 | j                   j                         ryt        j                  | j                   j	                               }t        |t              r;|j                         D ci c]  \  }}t        |t              s|| c}}| _        y y y c c}}w # t        t        f$ r2}t        j                  d| j                   |       i | _        Y d }~y d }~ww xY w)Nu7   dead_targets: could not load %s (%s) — starting empty)r   existsjsonloads	read_text
isinstancedictitemsr   OSError
ValueErrorloggerdebug)r   rawkvexcs        r   r   zDeadTargetRegistry._loadC   s    	zz  "jj!5!5!78c4( *-"!%A
1d8K1"DJ ) #" $ 	LLRS*DJJ	s0   A*B ,BB
	B B C+(CCc                   	 | j                   j                  j                  dd       | j                   j                  | j                   j                  dz         }|j                  t        j                  | j                  d             |j                  | j                          y # t        $ r+}t        j                  d| j                   |       Y d }~y d }~ww xY w)NT)parentsexist_okz.tmp   )indentz'dead_targets: could not persist %s (%s))r   parentmkdirwith_suffixsuffix
write_textr#   dumpsr   replacer)   r+   r,   )r   tmpr0   s      r   _flush_lockedz DeadTargetRegistry._flush_lockedQ   s    	UJJ##D4#@**(():):V)CDCNN4::djj;<KK

# 	ULLBDJJPSTT	Us   B$B' '	C0!CCc                ,    t        |       xr | t        v S )zEReturn True when ``error_kind`` denotes a permanent whole-chat death.)bool_DEAD_ERROR_KINDS)
error_kinds    r   is_dead_error_kindz%DeadTargetRegistry.is_dead_error_kind]   s     JCJ2C$CCr   c                |    |sy| j                   5  t        ||      | j                  v cd d d        S # 1 sw Y   y xY w)NF)r   r   r   )r   r   r   s      r   is_deadzDeadTargetRegistry.is_deadb   s7    ZZ 	?h0DJJ>	? 	? 	?s   2;c                   |syt        ||      }| j                  5  || j                  v }t        |      j	                         j                         t        |      t        |      dd t        j                         d| j                  |<   | j                          ddd       st        j                  d||xs d       | S # 1 sw Y   )xY w)z@Record a target as confirmed-dead.  Returns True if newly added.FN   )r   r   reason	marked_atuv   dead_targets: marked %s as unreachable (%s) — future deliveries to this target will be skipped until a send succeedszno reason given)
r   r   r   r   r   r   timer>   r+   info)r   r   r   rH   keyexisteds         r   	mark_deadzDeadTargetRegistry.mark_deadh   s     7+ZZ 	!TZZ'GM//1779w<f+ds+!YY[	DJJsO  	! KKGV00
 {	! 	!s   A?CCc                    |syt        ||      }| j                  5  || j                  v r=| j                  |= | j                          t        j                  d|       	 ddd       y	 ddd       y# 1 sw Y   yxY w)zHRemove a target's dead flag (self-healing).  Returns True if it was set.Fz3dead_targets: cleared %s (delivery succeeded again)NT)r   r   r   r>   r+   rK   )r   r   r   rL   s       r   clearzDeadTargetRegistry.clear   sx    7+ZZ 	djj JJsO""$QSVW	 	 	 	 s   AA22A;c           	         | j                   5  | j                  j                         D ci c]  \  }}|t        |       c}}cddd       S c c}}w # 1 sw Y   yxY w)zBSnapshot of the current dead set (for diagnostics / `hermes` CLI).N)r   r   r(   r'   )r   r.   r/   s      r   all_deadzDeadTargetRegistry.all_dead   sM    ZZ 	?+/::+;+;+=>41aAtAwJ>	? 	?>	? 	?s   AAAAA)N)r   zOptional[Path]returnNone)rS   rT   )rB   Optional[str]rS   r@   )r   r   r   rU   rS   r@   ) )r   r   r   rU   rH   r   rS   r@   )rS   zDict[str, Dict[str, object]])__name__
__module____qualname____doc__r    r   r>   staticmethodrC   rE   rN   rP   rR    r   r   r   r   0   sV    U D D? !#'+.?r   r   )r   r   r   r   rS   r   )rZ   
__future__r   r#   loggingr   rJ   pathlibr   typingr   r   hermes_cli.configr   	getLoggerrW   r+   	frozensetrA   r   r   r\   r   r   <module>rd      sZ   0 #      ! -			8	$ {K89 E
_? _?r   