
    JjP                        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m	Z	m
Z
  ej        e          Z G d d          Zd
d	ZdS )u  
Transport-agnostic WhatsApp behavior shared by the Baileys bridge adapter
and the official WhatsApp Cloud API adapter.

The mixin provides:
- Allow-list / DM / group gating
- Mention detection (explicit @-mentions + configurable regex patterns)
- Quoted-reply-to-bot detection
- Broadcast / Channel / Newsletter filtering
- WhatsApp-flavored markdown conversion
- Outgoing chunk length budgeting

It is the *behavior layer*. Transport-specific concerns (subprocess management,
HTTP webhooks, Graph API calls, media upload protocols) live in each adapter.

Mixin contract — the adapter must set these on ``self`` before any of the
mixin's methods are called (typically in ``__init__``):

    self.config        # gateway.config.PlatformConfig
    self.name          # str — adapter name (used in log lines)
    self._dm_policy             # str: "open" | "allowlist" | "disabled"
    self._allow_from            # set[str]
    self._group_policy          # str: "open" | "allowlist" | "disabled"
    self._group_allow_from      # set[str]
    self._mention_patterns      # list[re.Pattern]
    self._reply_prefix          # Optional[str]

Class attributes ``MAX_MESSAGE_LENGTH`` and ``DEFAULT_REPLY_PREFIX`` are
defined on the mixin and may be overridden per-adapter if needed.
    )annotationsN)AnyDictOptionalc                     e Zd ZU dZdZded<   dZdZded<    ej	        d	          Z
 ej	        d
          Zed.d            Zed/d            Zd0dZd1dZd/dZd2dZed2d            Zed3d            Zed4d            Zd/dZed5d            Zd6dZd6d Zd4d!Zd" Zd7d%Zd8d&Zd8d'Z d8d(Z!d9d*Z"d8d+Z#d.d,Z$d-S ):WhatsAppBehaviorMixinu"  Shared behavior for all WhatsApp adapters (Baileys + Cloud API).

    See module docstring for the attribute contract the host adapter must
    satisfy. This mixin owns no state of its own — every value it touches
    is either a class attribute or set by the adapter's ``__init__``.
    i   intMAX_MESSAGE_LENGTHTu8   ⚕ *Hermes Agent*
────────────
strDEFAULT_REPLY_PREFIXz[\u200b\u2060\u2063\ufeff]z3[\u00a0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]contentreturnc                v    |s|S | j                             d|          }| j                            d|          S )u  Remove invisible formatting chars that leak badly in WhatsApp.

        Some provider/gateway formatting paths can emit unicode like WORD
        JOINER (U+2060) plus NARROW NO-BREAK SPACE (U+202F). WhatsApp may
        render those as mojibake-looking prefixes (``⁠ text``) instead of
        invisible spacing. Keep normal text and emoji joiners intact, but
        strip known zero-width format chars and normalize odd unicode spaces.
          )_OUTBOUND_INVISIBLE_CHARS_REsub_OUTBOUND_ODD_SPACE_RE)clsr   s     G/home/rurouni/.hermes/hermes-agent/gateway/platforms/whatsapp_common.py_sanitize_outbound_textz-WhatsAppBehaviorMixin._sanitize_outbound_text>   sB      	N266r7CC)--c7;;;    boolc                    dS )zDWhatsApp gates DM/group access at intake via dm_policy/group_policy.T selfs    r   enforces_own_access_policyz0WhatsAppBehaviorMixin.enforces_own_access_policyM   s	     tr   c                    t          j        dd          }|dk    rdS | j        | j                            dd          S t          j        d          }||                    dd          S | j        S )zReturn the prefix to add to outgoing replies in self-chat mode.

        Subclasses that don't have a self-chat concept (the Cloud API
        adapter) can override this to always return ``""`` or apply a
        different policy.
        WHATSAPP_MODEz	self-chatr   Nz\n
WHATSAPP_REPLY_PREFIX)osgetenv_reply_prefixreplacer   )r   whatsapp_mode
env_prefixs      r   _effective_reply_prefixz-WhatsAppBehaviorMixin._effective_reply_prefixS   s|     	/;??K''2)%--eT:::Y677
!%%eT222((r   c                t    t          |                                           }t          d| j        |z
            S )z<Reserve room for the reply prefix so the final message fits.i   )lenr)   maxr
   )r   
prefix_lens     r   _outgoing_chunk_limitz+WhatsAppBehaviorMixin._outgoing_chunk_limitd   s5    557788
 40:=>>>r   c                
   | j         j                            d          }|:t          |t                    r|                                dv S t          |          S t          j        dd                                          dv S )Nrequire_mention>   1onyestrueWHATSAPP_REQUIRE_MENTIONfalse)	configextraget
isinstancer   lowerr   r#   r$   )r   
configureds     r   _whatsapp_require_mentionz/WhatsAppBehaviorMixin._whatsapp_require_mentionk   s    [&**+<==
!*c** H!''))-GGG
###y3W==CCEE J
 
 	
r   set[str]c                   | j         j                            d          }|t          j        dd          }t          |t                    rd |D             S d t          |                              d          D             S )Nfree_response_chatsWHATSAPP_FREE_RESPONSE_CHATSr   c                    h | ]D}t          |                                          #t          |                                          ES r   r   strip.0parts     r   	<setcomp>zFWhatsAppBehaviorMixin._whatsapp_free_response_chats.<locals>.<setcomp>}   =    KKK$T9J9JKCIIOO%%KKKr   c                ^    h | ]*}|                                 |                                 +S r   rD   rE   s     r   rH   zFWhatsAppBehaviorMixin._whatsapp_free_response_chats.<locals>.<setcomp>~   -    MMM

M

MMMr   ,)	r7   r8   r9   r#   r$   r:   listr   split)r   raws     r   _whatsapp_free_response_chatsz3WhatsAppBehaviorMixin._whatsapp_free_response_chatsx   s    k##$9::;):B??Cc4   	LKK#KKKKMMS)<)<MMMMr   c                    | t                      S t          | t                    rd | D             S d t          |                               d          D             S )z;Parse allow_from / group_allow_from from config or env var.Nc                    h | ]D}t          |                                          #t          |                                          ES r   rC   rE   s     r   rH   z;WhatsAppBehaviorMixin._coerce_allow_list.<locals>.<setcomp>   rI   r   c                ^    h | ]*}|                                 |                                 +S r   rK   rE   s     r   rH   z;WhatsAppBehaviorMixin._coerce_allow_list.<locals>.<setcomp>   rL   r   rM   )setr:   rN   r   rO   )rP   s    r   _coerce_allow_listz(WhatsAppBehaviorMixin._coerce_allow_list   s_     ;55Lc4   	LKK#KKKKMMS)<)<MMMMr   valueOptional[str]c                    | sdS t          |                                           }d|v rd|v r|                    ddd          }|S )Nr   :@   )r   rD   r&   )rW   
normalizeds     r   _normalize_whatsapp_idz,WhatsAppBehaviorMixin._normalize_whatsapp_id   sX     	2ZZ%%''
*
!2!2#++Ca88Jr   chat_idc                    | sdS |                                                                  }|dk    rdS |                    d          s|                    d          rdS dS )up  True for WhatsApp pseudo-chats that aren't real conversations.

        Covers Status updates (Stories) and Channel/Newsletter broadcasts.
        These show up as inbound messages on Baileys but the agent should
        never reply — answering a Story update spams the contact's status
        feed, and Channel posts aren't addressable in the first place.
        Fzstatus@broadcastTz
@broadcastz@newsletter)rD   r;   endswith)r_   cids     r   _is_broadcast_chatz(WhatsAppBehaviorMixin._is_broadcast_chat   sn      	5mmoo##%%$$$4 <<%% 	m)D)D 	4ur   c                    t          j        dd                                          dv rdS t          j        dd                                          dv S )NGATEWAY_ALLOW_ALL_USERSr   >   r1   r3   r4   TWHATSAPP_ALLOW_ALL_USERS)r#   r$   r;   r   s    r   _open_dm_opted_inz'WhatsAppBehaviorMixin._open_dm_opted_in   sQ    9.3399;;?SSS4y3R88>>@@DXXXr   	candidatec                    |sdS | |v rdS ddl m}m}  ||           }|sdS |D ],}|dk    r dS  ||          |v r dS  ||          |z  r dS -dS )am  Match a WhatsApp identifier against an allowlist across phone/LID forms.

        WhatsApp delivers inbound senders in LID form (``<id>@lid``) while
        operators usually configure allowlists with phone numbers, and vice
        versa. A raw set-membership check therefore never matches a known
        contact. Resolve both the candidate and each allowlist entry through
        the bridge's ``lid-mapping-*.json`` files (the shared
        ``gateway.whatsapp_identity`` helper that the gateway authz and
        session-key paths already use) so either configured form resolves to
        the inbound form.
        FTr   )expand_whatsapp_aliasesnormalize_whatsapp_identifier*)gateway.whatsapp_identityrj   rk   )rh   
allow_fromrj   rk   candidate_aliasesentrys         r   _matches_whatsapp_allowlistz1WhatsAppBehaviorMixin._matches_whatsapp_allowlist   s      	5 
""4	
 	
 	
 	
 	
 	
 	
 	

 43I>>  	5 	 	E||tt,,U337HHHtt '&u--0AA ttur   	sender_idc                    | j         dk    rdS | j         dk    r|                     || j                  S | j         dk    r|                                 S dS )u:   Strict DM authorization — pairing does not imply access.disabledF	allowlistopen)
_dm_policyrq   _allow_fromrg   )r   rr   s     r   _is_dm_allowedz$WhatsAppBehaviorMixin._is_dm_allowed   s^    ?j((5?k))33It?OPPP?f$$))+++ur   c                   t          |pd                                          }|sdS | j        dk    rdS | j        dk    r|                     || j                  S | j        dk    rdS | j        dk    r|                                 S dS )zCWhether a DM may reach the gateway intake (pairing handshake path).r   Frt   ru   pairingTrv   )r   rD   rw   rq   rx   rg   )r   rr   	principals      r   _is_dm_intake_allowedz+WhatsAppBehaviorMixin._is_dm_intake_allowed   s    	R((..00	 	5?j((5?k))33It?OPPP?i''4?f$$))+++ur   c                    | j         dk    rdS | j         dk    r|                     || j                  S | j         dk    rdS | j         dk    rdS dS )z/Check whether a group chat should be processed.rt   Fru   r{   rv   T)_group_policyrq   _group_allow_from)r   r_   s     r   _is_group_allowedz'WhatsAppBehaviorMixin._is_group_allowed   sg    ++5,,33GT=STTT**5''4ur   c                   | j         j                            d          }|t          j        dd                                          }|re	 t          j        |          }nO# t          $ rB d |	                                D             }|sd |
                    d          D             }Y nw xY w|g S t          |t                    r|g}t          |t                    s5t                              d| j        t#          |          j                   g S g }|D ]}t          |t                    r|                                s,	 |                    t)          j        |t(          j                             `# t(          j        $ r,}t                              d| j        ||           Y d }~d }~ww xY w|r.t                              d	| j        t3          |                     |S )
Nmention_patternsWHATSAPP_MENTION_PATTERNSr   c                ^    g | ]*}|                                 |                                 +S r   rK   rE   s     r   
<listcomp>zCWhatsAppBehaviorMixin._compile_mention_patterns.<locals>.<listcomp>  s=          )-TZZ\\ 

     r   c                ^    g | ]*}|                                 |                                 +S r   rK   rE   s     r   r   zCWhatsAppBehaviorMixin._compile_mention_patterns.<locals>.<listcomp>  s=     $ $ $-1tzz||$ JJLL$ $ $r   rM   z?[%s] whatsapp mention_patterns must be a list or string; got %sz,[%s] Invalid WhatsApp mention pattern %r: %sz*[%s] Loaded %d WhatsApp mention pattern(s))r7   r8   r9   r#   r$   rD   jsonloads	Exception
splitlinesrO   r:   r   rN   loggerwarningnametype__name__appendrecompile
IGNORECASEerrorinfor+   )r   patternsrP   compiledpatternexcs         r   _compile_mention_patternsz/WhatsAppBehaviorMixin._compile_mention_patterns   s/   ;$(();<<)7<<BBDDC 
	#z#HH       141A1A     H $ $ $58YYs^^$ $ $ Ih$$ 	" zH(D)) 	NNQ	X'  
 I 	 	Ggs++ 7==?? 
7BM B BCCCC8   BI	         	KK<diX   s+   A! !A	B-,B-2E::F5	"F00F5dataDict[str, Any]c                    t                      }|                    d          pg D ].}|                     |          }|r|                    |           /|S )NbotIds)rU   r9   r^   add)r   r   bot_idsrh   r]   s        r   _bot_ids_from_messagez+WhatsAppBehaviorMixin._bot_ids_from_message'  s^    %%(++1r 	( 	(I44Y??J (J'''r   c                    |                      |                    d                    }|sdS ||                     |          v S )NquotedParticipantF)r^   r9   r   )r   r   quoted_participants      r   _message_is_reply_to_botz.WhatsAppBehaviorMixin._message_is_reply_to_bot/  sI    !88BU9V9VWW! 	5!T%?%?%E%EEEr   c                                          |          }|sdS  fd|                    d          pg D             }||z  rdS t          |                    d          pd          }|                                }|D ]@}|                    dd          d	                                         }|rd| |v s||v r dS AdS )
NFc                B    h | ]}                     |          xS r   )r^   )rF   rh   nidr   s     r   rH   z>WhatsAppBehaviorMixin._message_mentions_bot.<locals>.<setcomp>9  sB     
 
 
229===

 
 
r   mentionedIdsTbodyr   r[   r\   r   )r   r9   r   r;   rO   )	r   r   r   mentioned_idsr   
lower_bodybot_idbare_idr   s	   `       @r   _message_mentions_botz+WhatsAppBehaviorMixin._message_mentions_bot5  s   ,,T22 	5
 
 
 
 
"hh~66<"
 
 

 7" 	4488F##)r**ZZ\\
 	 	Fll3**1-3355G MMMZ777j;P;Pttur   c                    | j         sdS t          |                    d          pd          t          fd| j         D                       S )NFr   r   c              3  B   K   | ]}|                               V  d S )N)search)rF   r   r   s     r   	<genexpr>zJWhatsAppBehaviorMixin._message_matches_mention_patterns.<locals>.<genexpr>M  s/      NNG7>>$''NNNNNNr   )_mention_patternsr   r9   any)r   r   r   s     @r   !_message_matches_mention_patternsz7WhatsAppBehaviorMixin._message_matches_mention_patternsI  sW    % 	5488F##)r**NNNNt7MNNNNNNr   textc                   |s|S |                      |          }|}|D ]L}|                    dd          d         }|r,t          j        dt          j        |           dd|          }M|                                p|S )Nr[   r\   r   z\b[,:\-]*\s*r   )r   rO   r   r   escaperD   )r   r   r   r   cleanedr   r   s          r   _clean_bot_mention_textz-WhatsAppBehaviorMixin._clean_bot_mention_textO  s     	K,,T22 	 	Fll3**1-G &97++9992w  }}&$&r   c                D   t          |                    d          pd          }|                     |          rdS |                    dd          }|r|}|                     |          sdS nRt          |                    d          p|                    d          pd          }|                     |          sdS dS t          |                    d          pd          }||                                 v rdS |                                 sdS t          |                    d          pd                                          }|                    d	          rdS | 	                    |          rdS | 
                    |          rdS |                     |          S )
NchatIdr   FisGroupsenderIdfromTr   /)r   r9   rc   r   r}   rQ   r=   rD   
startswithr   r   r   )r   r   chat_id_rawis_groupr_   rr   r   s          r   _should_process_messagez-WhatsAppBehaviorMixin._should_process_message\  s   $((8,,233
 "";// 	588Iu-- 		!G))'22 u DHHZ00JDHHV4D4DJKKI--i88 u4dhhx((.B//d88::::4--// 	4488F##)r**0022??3 	4((.. 	4%%d++ 	455d;;;r   c                  	
 |s|S |                      |          }d
g d
fd}t          j        d||          }d	g d	fd	}t          j        d
||          }t          j        dd|          }t          j        dd|          }t          j        dd|          }t          j        dd|          }dd}t          j        d||t          j                  }t          j        dd|          }t	                    D ] \  }}|                    
 | d|          }!t	                    D ] \  }}|                    	 | d|          }!|S )a  Convert standard markdown to WhatsApp-compatible formatting.

        WhatsApp supports: *bold*, _italic_, ~strikethrough~, ```code```,
        and monospaced `inline`. Standard markdown uses different syntax
        for bold/italic/strikethrough, so we convert here.

        Code blocks (``` fenced) and inline code (`) are protected from
        conversion via placeholder substitution.
        z FENCEmre.Matchr   r   c                                         |                     d                      t                    dz
   dS Nr   r\    r   groupr+   )r   	_FENCE_PHfencess    r   _save_fencez9WhatsAppBehaviorMixin.format_message.<locals>._save_fence  s=    MM!''!**%%%6Vq6666r   z```[\s\S]*?```z CODEc                                         |                     d                      t                    dz
   dS r   r   )r   _CODE_PHcodess    r   
_save_codez8WhatsAppBehaviorMixin.format_message.<locals>._save_code  s=    LL$$$4E

Q4444r   z	`[^`\n]+`z.(?<!\*)\*(?!\s|\*)([^*\n]*?\S[^*\n]*?)\*(?!\*)z_\1_z\*\*(.+?)\*\*z*\1*z	__(.+?)__z	~~(.+?)~~z~\1~c                   |                      d                                          }t          |          dk    r|                    d          rn|                    d          rY|dd                                         }t          |          dk    r*|                    d          r|                    d          Yd| dS )Nr\   rl   )r   rD   r+   r   ra   )r   inners     r   _header_to_boldz=WhatsAppBehaviorMixin.format_message.<locals>._header_to_bold  s    GGAJJ$$&&Ee**q..U%5%5c%:%:.u~~c?R?R.ad))++ e**q..U%5%5c%:%:.u~~c?R?R.u<<<r   z^#{1,6}\s+(.+)$)flagsz\[([^\]]+)\]\(([^)]+)\)z\1 (\2)r   )r   r   r   r   )r   r   r   	MULTILINE	enumerater&   )r   r   r   resultr   r   ifencecoder   r   r   r   s            @@@@r   format_messagez$WhatsAppBehaviorMixin.format_message  s     	N..w77  		7 	7 	7 	7 	7 	7 	7 );@@ 	5 	5 	5 	5 	5 	5 	5 j&99 =
 
 ('6::gv66gv66	  	  	  	  r|
 
 

 2JGG "&)) 	B 	BHAu^^y$9!$9$9$95AAFF '' 	@ 	@GAt^^x$8$8$8$8$??FFr   N)r   r   r   r   )r   r   )r   r   )r   r	   )r   r>   )rW   rX   r   r   )r_   r   r   r   )rh   r   r   r   )rr   r   r   r   )r   r   r   r>   )r   r   r   r   )r   r   r   r   r   r   )%r   
__module____qualname____doc__r
   __annotations__supports_code_blocksr   r   r   r   r   classmethodr   propertyr   r)   r.   r=   rQ   staticmethodrV   r^   rc   rg   rq   ry   r}   r   r   r   r   r   r   r   r   r   r   r   r   r   r   ,   s          #"""" \\\\\#-2:.K#L#L 'RZ(^__< < < [<    X
) ) ) )"? ? ? ?
 
 
 
N N N N N N N \N    \    \(Y Y Y Y
 $ $ $ \$L      
 
 
 
, , ,\   F F F F   (O O O O' ' ' ' <  <  <  <FJ J J J J Jr   r   r   Pathc                    ddl } ddlm} ddlm}  |t
                                                    j        d         dz  dz  } |            }|dz  dz  }	 |dz  }|                                 |	                                 d	}n# t          t          f$ r d
}Y nw xY w|r|S |                                r|S 	 |j                            d	d	           |                     ||d
           |S # t           $ r |cY S w xY w)ae  Resolve the WhatsApp bridge directory, mirroring to HERMES_HOME if needed.

    When the install tree is read-only (e.g., Docker /opt/hermes), this function
    mirrors the bridge source to a writable HERMES_HOME location and returns that
    path. This ensures npm install works in Docker environments.

    Returns the resolved bridge directory path.
    r   N)r   )get_hermes_home   scriptszwhatsapp-bridgez.write_testTF)parentsexist_ok)dirs_exist_ok)shutilpathlibr   hermes_constantsr   __file__resolver   touchunlinkOSErrorPermissionErrorexistsparentmkdircopytreer   )r   _Pathr   install_bridgehermes_homehermes_home_bridge	test_fileinstall_writables           r   resolve_whatsapp_bridge_dirr    s    MMM%%%%%% 100000U8__,,..6q9IEHYYN "/##K$y03DD!"]2	_% ! ! ! !     "" "!!	!''t'DDD 	 	
 	
 	

 "!   s$   /B BB;5C1 1D ?D )r   r   )r   
__future__r   r   loggingr#   r   typingr   r   r   	getLoggerr   r   r   r  r   r   r   <module>r
     s    > # " " " " "   				 				 & & & & & & & & & & 
	8	$	$] ] ] ] ] ] ] ]H. . . . . .r   