
    (GjL6                        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mZ ddlm	Z	m
Z
 ddlmZ e	rddlmZ  ej        e          Z G d d          Z G d	 d
          Z ej        dej                  Z ej        dej                  Z ej        dej                  Z ej        dej                  Z ej        d          Z ej        d          Z ej        dej                  Z ej        d          Z ej        d          Zde de fdZ! G d d          Z"de de fdZ# ej        d          Z$de de%fdZ&de de'e          fdZ(de'e          de fd Z)de de fd!Z*dS )"zShared helper classes for gateway platform adapters.

Extracts common patterns that were duplicated across 5-7 adapters:
message deduplication, text batch aggregation, markdown stripping,
and thread participation tracking.
    N)Path)TYPE_CHECKINGDict)atomic_json_write)MessageEventc                   :    e Zd ZdZddedefdZdedefd	Z	d
 Z
dS )MessageDeduplicatora|  TTL-based message deduplication cache.

    Replaces the identical ``_seen_messages`` / ``_is_duplicate()`` pattern
    previously duplicated in discord, slack, dingtalk, wecom, weixin,
    mattermost, and feishu adapters.

    Usage::

        self._dedup = MessageDeduplicator()

        # In message handler:
        if self._dedup.is_duplicate(msg_id):
            return
      ,  max_sizettl_secondsc                 0    i | _         || _        || _        d S N)_seen	_max_size_ttl)selfr   r   s      ?/home/rurouni/.hermes/hermes-agent/gateway/platforms/helpers.py__init__zMessageDeduplicator.__init__+   s    ')
!			    msg_idreturnc                    |sdS t          j                     }|| j        v r#|| j        |         z
  | j        k     rdS | j        |= || j        |<   t          | j                  | j        k    r|| j        z
  fd| j                                        D             | _        t          | j                  | j        k    rKt          | j                                        d           | j         d         }t          |          | _        dS )z?Return True if *msg_id* was already seen within the TTL window.FTc                 (    i | ]\  }}|k    ||S  r   ).0kvcutoffs      r   
<dictcomp>z4MessageDeduplicator.is_duplicate.<locals>.<dictcomp>=   s$    LLL41aV!Qr   c                     | d         S )N   r   )items    r   <lambda>z2MessageDeduplicator.is_duplicate.<locals>.<lambda>D   s
    T!W r   )keyN)timer   r   lenr   itemssorteddict)r   r   nownewestr   s       @r   is_duplicatez MessageDeduplicator.is_duplicate0   s    	5ikkTZTZ''$)33t
6" 
6tz??T^++49_FLLLL4:+;+;+=+=LLLDJ4://  J$$&&,,   >/""$ "&\\
ur   c                 8    | j                                          dS )zClear all tracked messages.N)r   clearr   s    r   r/   zMessageDeduplicator.clearI   s    
r   N)r
   r   )__name__
__module____qualname____doc__intfloatr   strboolr-   r/   r   r   r   r	   r	      sv              %        
3 4    2    r   r	   c                   h    e Zd ZdZdddddededefd	Zd
efdZddde	d
dfdZ
de	d
dfdZddZdS )TextBatchAggregatora@  Aggregates rapid-fire text events into single messages.

    Replaces the ``_enqueue_text_event`` / ``_flush_text_batch`` pattern
    previously duplicated in telegram, discord, matrix, wecom, and feishu.

    Usage::

        self._text_batcher = TextBatchAggregator(
            handler=self._message_handler,
            batch_delay=0.6,
            split_threshold=1900,
        )

        # In message dispatch:
        if msg_type == MessageType.TEXT and self._text_batcher.is_enabled():
            self._text_batcher.enqueue(event, session_key)
            return
    g333333?g       @i  )batch_delaysplit_delaysplit_thresholdr;   r<   r=   c                Z    || _         || _        || _        || _        i | _        i | _        d S r   )_handler_batch_delay_split_delay_split_threshold_pending_pending_tasks)r   handlerr;   r<   r=   s        r   r   zTextBatchAggregator.__init__e   s8      '' /3579r   r   c                     | j         dk    S )z.Return True if batching is active (delay > 0).r   )r@   r0   s    r   
is_enabledzTextBatchAggregator.is_enabledt   s     1$$r   eventr   r%   Nc                    t          |j        pd          }| j                            |          }|s||_        || j        |<   n|j         d|j         |_        ||_        | j                            |          }|r(|                                s|                                 t          j	        | 
                    |                    | j        |<   dS )z+Add *event* to the pending batch for *key*. 
N)r'   textrC   get_last_chunk_lenrD   donecancelasynciocreate_task_flush)r   rH   r%   	chunk_lenexistingpriors         r   enqueuezTextBatchAggregator.enqueuex   s    
(b))	=$$S)) 	1$-E!!&DM#'}<<
<<HM'0H$ #'',, 	 	LLNNN#*#6t{{37G7G#H#HC   r   c                 >  K   | j                             |          }| j                            |          }|rt          |dd          nd}|| j        k    r| j        n| j        }t          j        |           d{V  | j        	                    |d          }|rH	 | 
                    |           d{V  n+# t          $ r t                              d|           Y nw xY w| j                             |          |u r| j         	                    |d           dS dS )z/Wait then dispatch the batched event for *key*.rN   r   Nz<[TextBatchAggregator] Error dispatching batched event for %s)rD   rM   rC   getattrrB   rA   r@   rQ   sleeppopr?   	Exceptionlogger	exception)r   r%   current_taskpendinglast_lendelayrH   s          r   rS   zTextBatchAggregator._flush   s]     *..s33-##C((=DK77$5q999! &.1F%F%F!!DL]mE"""""""""!!#t,, 	ffmmE********** f f f  !_adeeeeef ""3''<77##C..... 87s   B9 9%C! C!c                     | j                                         D ]*}|                                s|                                 +| j                                          | j                                         dS )zCancel all pending flush tasks.N)rD   valuesrO   rP   r/   rC   )r   tasks     r   
cancel_allzTextBatchAggregator.cancel_all   sm    '..00 	 	D99;; !!###r   r   N)r1   r2   r3   r4   r6   r5   r   r8   rG   r7   rW   rS   rf   r   r   r   r:   r:   Q   s         . ! #: : : 	:
 : : : : :%D % % % %I^ I# I$ I I I I"/ / / / / /(     r   r:   z\*\*(.+?)\*\*z	\*(.+?)\*z \b__(?![\s_])(.+?)(?<![\s_])__\bz\b_(?![\s_])(.+?)(?<![\s_])_\bz```[a-zA-Z0-9_+-]*\n?z`(.+?)`z
^#{1,6}\s+z\[([^\]]+)\]\([^\)]+\)z\n{3,}rL   r   c                    t                               d|           } t                              d|           } t                              d|           } t                              d|           } t
                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } | 
                                S )zStrip markdown formatting for plain-text platforms (SMS, iMessage, etc.).

    Replaces the identical ``_strip_markdown()`` functions previously
    duplicated in sms.py, bluebubbles.py, and feishu.py.
    z\1rJ   

)_RE_BOLDsub_RE_ITALIC_STAR_RE_BOLD_UNDER_RE_ITALIC_UNDER_RE_CODE_BLOCK_RE_INLINE_CODE_RE_HEADING_RE_LINK_RE_MULTI_NEWLINEstrip)rL   s    r   strip_markdownru      s     <<t$$Dud++DeT**Dt,,Db$''Dud++D??2t$$D<<t$$D  ..D::<<r   c                   |    e Zd ZdZdZddedefdZdefdZ	de
e         fdZdd
Zdedd	fdZdedefdZddZd	S )ThreadParticipationTrackera  Persistent tracking of threads the bot has participated in.

    Replaces the identical ``_load/_save_participated_threads`` +
    ``_mark_thread_participated`` pattern previously duplicated in
    discord.py and matrix.py.

    Usage::

        self._threads = ThreadParticipationTracker("discord")

        # Check membership:
        if thread_id in self._threads:
            ...

        # Mark participation:
        self._threads.mark(thread_id)
      platform_namemax_trackedc                 h    || _         || _        d |                                 D             | _        d S )Nc                 .    i | ]}t          |          d S r   r7   r   	thread_ids     r   r    z7ThreadParticipationTracker.__init__.<locals>.<dictcomp>   s-     *
 *
 *
%.C	NND*
 *
 *
r   )	_platform_max_tracked_load_threads)r   ry   rz   s      r   r   z#ThreadParticipationTracker.__init__   s=    &'*
 *
26**,,*
 *
 *
r   r   c                 8    ddl m}  |            | j         dz  S )Nr   )get_hermes_homez_threads.json)hermes_constantsr   r   )r   r   s     r   _state_pathz&ThreadParticipationTracker._state_path   s2    444444  dn#C#C#CCCr   c                    |                                  }|                                r[	 t          j        |                    d                    }t          |t                    rd |D             S n# t          $ r Y nw xY wg S )Nzutf-8)encodingc                 ,    g | ]}t          |          S r   r}   r~   s     r   
<listcomp>z4ThreadParticipationTracker._load.<locals>.<listcomp>   s    AAAyC	NNAAAr   )r   existsjsonloads	read_text
isinstancelistr\   )r   pathdatas      r   r   z ThreadParticipationTracker._load   s    !!;;== 	z$..'."B"BCCdD)) BAADAAAAB   	s   AA4 4
B BNc                    |                                  }t          | j                  }t          |          | j        k    r/|| j         d          }t
                              |          | _        t          ||d            d S )N)indent)r   r   r   r'   r   r*   fromkeysr   )r   r   thread_lists      r   _savez ThreadParticipationTracker._save   s|    !!4=)){d///%t'8&8&9&9:K MM+66DM$D999999r   r   c                 X    || j         vr d| j         |<   |                                  dS dS )z-Mark *thread_id* as participated and persist.N)r   r   r   r   s     r   markzThreadParticipationTracker.mark   s4    DM))'+DM)$JJLLLLL *)r   c                     || j         v S r   )r   r   s     r   __contains__z'ThreadParticipationTracker.__contains__  s    DM))r   c                 8    | j                                          d S r   )r   r/   r0   s    r   r/   z ThreadParticipationTracker.clear  s    r   )rx   rg   )r1   r2   r3   r4   _MAX_TRACKEDr7   r5   r   r   r   r   r   r   r   r8   r   r/   r   r   r   rw   rw      s         $ L
 
c 
 
 
 
 
DT D D D D	tCy 	 	 	 	: : : :c d    *c *d * * * *     r   rw   phonec                     | sdS t          |           dk    r-t          |           dk    r| dd         dz   | dd         z   ndS | dd         dz   | dd         z   S )	zRedact a phone number for logging, preserving country code and last 4.

    Replaces the identical ``_redact_phone()`` functions in signal.py,
    sms.py, and bluebubbles.py.
    z<none>      N   z****)r'   )r   s    r   redact_phoner     ss      x
5zzQ25e**q..uRaRy6!E"##J..fL!9vbcc
**r   z0^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*){1,}\|?\s*$linec                 P    |                                  }t          |          od|v S )z:Return True if *line* could plausibly be a table data row.|)rt   r8   r   strippeds     r   is_table_rowr   '  s$    zz||H>>-cXo-r   c                     |                                  }|                    d          r
|dd         }|                    d          r
|dd         }d |                    d          D             S )z0Split a GFM table row into stripped cell values.r   r"   Nc                 6    g | ]}|                                 S r   )rt   r   cells     r   r   z,split_markdown_table_row.<locals>.<listcomp>4  s     999TDJJLL999r   )rt   
startswithendswithsplitr   s     r   split_markdown_table_rowr   -  su    zz||H3  ABB< !CRC=99X^^C%8%89999r   table_blockc                 p   t          |           dk     rd                    |           S t          | d                   }t          |          dk     rd                    |           S t          |           dk    rt          | d                   ng }t          |          t          |          dz   k    }g }t          | dd         d          D ]S\  }}t          |          }|r"|r|d         r|d         nd| }|dd         }	nt	          d	 |D             d|           }|}	t          |	          t          |          k     r7|	                    d
gt          |          t          |	          z
  z             n7t          |	          t          |          k    r|	dt          |                   }	g }
t          ||	          D ])\  }}|s||k    r|
                    d| d|            *d| dg|
}|                    d                    |                     Ud                    |          S )u4  Render a detected GFM table as bold-heading + bullet groups.

    Uses the same alignment logic as Telegram's renderer: for non-row-label
    tables, ``data_cells = cells`` (the full row) and the bullet whose value
    duplicates the heading is skipped.  This keeps header→value alignment
    correct.
       rK   r   r   r"   N)startzRow c              3      K   | ]}||V  	d S r   r   r   s     r   	<genexpr>z&_render_table_block.<locals>.<genexpr>T  s'      ;;Td;D;;;;;;r   rJ   u   • z: z**ri   )r'   joinr   	enumeratenextextendzipappend)r   headersfirst_data_rowhas_row_label_colrendered_groupsindexrowcellsheading
data_cellsbulletsheadervaluegroup_liness                 r   _render_table_blockr   7  sd    ;!yy%%%&{1~66G
7||ayy%%% {a 	!Q000 
 N++s7||a/??!#OABBq999 7 7
s(-- 	"'HE!HHeAhh...GqrrJJ;;U;;;^E^^LLGJz??S\\))rdc'llS__&DEFFFF__s7||++#Nc'llN3J *55 	5 	5MFE$ ')9)9NN3&33E334444'G'''2'2tyy556666;;'''r   c                    d| vsd| vr| S |                      d          }g }d}d}|t          |          k     r||         }|                                }|                    d          r| }|                    |           |dz  }c|r|                    |           |dz  }d|v r|dz   t          |          k     rt
                              ||dz                      r|||dz            g}|dz   }|t          |          k     r]t          ||                   rH|                    ||                    |dz  }|t          |          k     rt          ||                   H|                    t          |                     |}e|                    |           |dz  }|t          |          k     d	                    |          S )	zuRewrite GFM pipe tables into bold-heading + bullet groups.

    Tables inside fenced code blocks are left alone.
    r   -rK   Fr   z```r"   r   )
r   r'   lstripr   r   TABLE_SEPARATOR_REmatchr   r   r   )	rL   linesoutin_fenceir   r   r   js	            r   convert_table_to_bulletsr   h  s   
 $#T//JJtECH	A
c%jj..Qx;;==u%% 	#|HJJtFA 	JJtFA 4KKAE

"""((q1u66 #  q1u.KAAc%jj..\%(%;%;.""58,,,Q c%jj..\%(%;%;. JJ*;77888A

4	Q; c%jj..> 99S>>r   )+r4   rQ   r   loggingrer&   pathlibr   typingr   r   utilsr   gateway.platforms.baser   	getLoggerr1   r]   r	   r:   compileDOTALLrj   rl   rm   rn   ro   rp   	MULTILINErq   rr   rs   r7   ru   rw   r   r   r8   r   r   r   r   r   r   r   r   <module>r      s       				        & & & & & & & & # # # # # # 4333333		8	$	$0 0 0 0 0 0 0 0lR R R R R R R Rp 2:&	22"*\2955?KK2:?KK 455"*Z((bj552:/00BJy))      *= = = = = = = =F
+ 
+ 
+ 
+ 
+ 
+,  RZ7  
.s .t . . . .:3 :49 : : : :.(T#Y .(3 .( .( .( .(b+3 +3 + + + + + +r   