
    `gj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y)	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                  d   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d$dZd%dZd#dZd&dZed&d       Zed'd       Zed(d       Zd#dZed)d       Zd*dZd*dZd(dZd Zd+dZd,dZd,dZ d,dZ!d-dZ"d,dZ#d"d Z$y!).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]c                z    |s|S | j                   j                  d|      }| j                  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)clscontents     T/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/whatsapp_common.py_sanitize_outbound_textz-WhatsAppBehaviorMixin._sanitize_outbound_text>   s=     N2266r7C))--c7;;    c                     y)zDWhatsApp gates DM/group access at intake via dm_policy/group_policy.T selfs    r   enforces_own_access_policyz0WhatsAppBehaviorMixin.enforces_own_access_policyM   s     r   c                    t        j                  dd      }|dk7  ry| j                  | j                  j                  dd      S t        j                  d      }||j                  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   z\n
WHATSAPP_REPLY_PREFIX)osgetenv_reply_prefixreplacer   )r   whatsapp_mode
env_prefixs      r   _effective_reply_prefixz-WhatsAppBehaviorMixin._effective_reply_prefixS   sx     		/;?K')%%--eT::YY67
!%%eT22(((r   c                f    t        | j                               }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   s/    5578
 400:=>>r   c                    | j                   j                  j                  d      }|-t        |t              r|j                         dv S t        |      S t        j                  dd      j                         dv S )Nrequire_mention>   1onyestrueWHATSAPP_REQUIRE_MENTIONfalse)	configextraget
isinstancer   lowerboolr!   r"   )r   
configureds     r   _whatsapp_require_mentionz/WhatsAppBehaviorMixin._whatsapp_require_mentionk   su    [[&&**+<=
!*c*!'')-GGG
##yy3W=CCE J
 
 	
r   c                   | j                   j                  j                  d      }|t        j                  dd      }t        |t              rA|D ch c]5  }t        |      j                         st        |      j                         7 c}S t        |      j                  d      D ch c]#  }|j                         s|j                         % c}S c c}w c c}w )Nfree_response_chatsWHATSAPP_FREE_RESPONSE_CHATSr   ,)
r5   r6   r7   r!   r"   r8   listr   stripsplit)r   rawparts      r   _whatsapp_free_response_chatsz3WhatsAppBehaviorMixin._whatsapp_free_response_chatsx   s    kk##$9:;)):B?Cc4 25K$T9JCIOO%KK),S)<M



MM LMs   C1C+CCc                ^   | 
t               S t        | t              rA| D ch c]5  }t        |      j	                         st        |      j	                         7 c}S t        |       j                  d      D ch c]#  }|j	                         s|j	                         % c}S c c}w c c}w )z;Parse allow_from / group_allow_from from config or env var.r@   )setr8   rA   r   rB   rC   )rD   rE   s     r   _coerce_allow_listz(WhatsAppBehaviorMixin._coerce_allow_list   sx     ;5Lc4 25K$T9JCIOO%KK),S)<M



MM LMs   B% B%:B*B*c                t    | syt        |       j                         }d|v rd|v r|j                  ddd      }|S )Nr   :@   )r   rB   r$   )value
normalizeds     r   _normalize_whatsapp_idz,WhatsAppBehaviorMixin._normalize_whatsapp_id   sC    Z%%'
*
!2#++Ca8Jr   c                    | sy| j                         j                         }|dk(  ry|j                  d      s|j                  d      ryy)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)rB   r9   endswith)chat_idcids     r   _is_broadcast_chatz(WhatsAppBehaviorMixin._is_broadcast_chat   sI     mmo##%$$ <<%m)Dr   c                    t        j                  dd      j                         dv ryt        j                  dd      j                         dv S )NGATEWAY_ALLOW_ALL_USERSr   >   r/   r1   r2   TWHATSAPP_ALLOW_ALL_USERS)r!   r"   r9   r   s    r   _open_dm_opted_inz'WhatsAppBehaviorMixin._open_dm_opted_in   sD    99.399;?SSyy3R8>>@DXXXr   c                    |sy| |v ryddl m}m}  ||       }|sy|D ]"  }|dk(  r y ||      |v r y ||      |z  s" y y)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_identityr[   r\   )	candidate
allow_fromr[   r\   candidate_aliasesentrys         r   _matches_whatsapp_allowlistz1WhatsAppBehaviorMixin._matches_whatsapp_allowlist   sq      
"	

 4I>  	E|,U37HH 'u-0AA	 r   c                    | j                   dk(  ry| j                   dk(  r| j                  || j                        S | j                   dk(  r| j                         S y)u:   Strict DM authorization — pairing does not imply access.disabledF	allowlistopen)
_dm_policyrc   _allow_fromrY   )r   	sender_ids     r   _is_dm_allowedz$WhatsAppBehaviorMixin._is_dm_allowed   sT    ??j(??k)33It?O?OPP??f$))++r   c                   t        |xs d      j                         }|sy| j                  dk(  ry| j                  dk(  r| j                  || j                        S | j                  dk(  ry| j                  dk(  r| j                         S y)zCWhether a DM may reach the gateway intake (pairing handshake path).r   Fre   rf   pairingTrg   )r   rB   rh   rc   ri   rY   )r   rj   	principals      r   _is_dm_intake_allowedz+WhatsAppBehaviorMixin._is_dm_intake_allowed   s    	R(..0	??j(??k)33It?O?OPP??i'??f$))++r   c                    | j                   dk(  ry| j                   dk(  r| j                  || j                        S | j                   dk(  ry| j                   dk(  ryy)z/Check whether a group chat should be processed.re   Frf   rm   rg   T)_group_policyrc   _group_allow_from)r   rS   s     r   _is_group_allowedz'WhatsAppBehaviorMixin._is_group_allowed   s_    +,33GT=S=STT*'r   c                N   | j                   j                  j                  d      }|<t        j                  dd      j                         }|r	 t        j                  |      }|g S t        |t              r|g}t        |t              s6t        j                  d| j                   t#        |      j$                         g S g }|D ]W  }t        |t              r|j                         s$	 |j'                  t)        j*                  |t(        j,                               Y |r*t        j1                  d| j                   t3        |             |S # t        $ r |j                         D cg c]#  }|j                         s|j                         % nc c}w }}|sC|j                  d      D cg c]#  }|j                         s|j                         % nc c}w }}Y |w xY w# t(        j.                  $ r-}t        j                  d| j                   ||       Y d }~Zd }~ww xY w)Nmention_patternsWHATSAPP_MENTION_PATTERNSr   r@   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))r5   r6   r7   r!   r"   rB   jsonloads	Exception
splitlinesrC   r8   r   rA   loggerwarningnametype__name__appendrecompile
IGNORECASEerrorinfor)   )r   patternsrD   rE   compiledpatternexcs          r   _compile_mention_patternsz/WhatsAppBehaviorMixin._compile_mention_patterns   s   ;;$$(();<))7<BBDC	#zz#H Ih$ zH(D)NNQ		X''
 I 	Ggs+7==?

7BMM BC		 KK<diiX K ! 141A )-TZZ\

   H   $58YYs^$-1tzz|DJJL$ $ $4 88 BII	 sN   E *3G$G!(F>FG!-GGG! G!$H$7"HH$c                    t               }|j                  d      xs g D ]'  }| j                  |      }|s|j                  |       ) |S )NbotIds)rH   r7   rP   add)r   databot_idsr_   rO   s        r   _bot_ids_from_messagez+WhatsAppBehaviorMixin._bot_ids_from_message'  sL    %(+1r 	(I44Y?JJ'	( r   c                n    | j                  |j                  d            }|sy|| j                  |      v S )NquotedParticipantF)rP   r7   r   )r   r   quoted_participants      r   _message_is_reply_to_botz.WhatsAppBehaviorMixin._message_is_reply_to_bot/  s9    !88BU9VW!!T%?%?%EEEr   c                |   | j                  |      }|sy|j                  d      xs g D ch c]  }| j                  |      x}r| }}||z  ryt        |j                  d      xs d      }|j	                         }|D ]5  }|j                  dd      d   j	                         }	|	s)d|	 |v s|	|v s5 y yc c}w )	NFmentionedIdsTbodyr   rL   rM   r   )r   r7   rP   r   r9   rC   )
r   r   r   r_   nidmentioned_idsr   
lower_bodybot_idbare_ids
             r   _message_mentions_botz+WhatsAppBehaviorMixin._message_mentions_bot5  s    ,,T2 #hh~6<"
229=== 
 

 7"488F#)r*ZZ\
 	Fll3*1-335GayMZ77j;P	 
s   B9c                    | j                   syt        |j                  d      xs d      t        fd| j                   D              S )NFr   r   c              3  @   K   | ]  }|j                          y w)N)search).0r   r   s     r   	<genexpr>zJWhatsAppBehaviorMixin._message_matches_mention_patterns.<locals>.<genexpr>M  s     NG7>>$'Ns   )_mention_patternsr   r7   any)r   r   r   s     @r   !_message_matches_mention_patternsz7WhatsAppBehaviorMixin._message_matches_mention_patternsI  s>    %%488F#)r*Nt7M7MNNNr   c                    |s|S | j                  |      }|}|D ]H  }|j                  dd      d   }|st        j                  dt        j                  |       dd|      }J |j                         xs |S )NrL   rM   r   z\b[,:\-]*\s*r   )r   rC   r   r   escaperB   )r   textr   r   cleanedr   r   s          r   _clean_bot_mention_textz-WhatsAppBehaviorMixin._clean_bot_mention_textO  s    K,,T2 	Fll3*1-G&&7+,L92w	 }}&$&r   c                   t        |j                  d      xs d      }| j                  |      ry|j                  dd      }|r|}| j                  |      sEyt        |j                  d      xs |j                  d      xs d      }| j	                  |      syyt        |j                  d      xs d      }|| j                         v ry| j                         syt        |j                  d      xs d      j                         }|j                  d	      ry| j                  |      ry| j                  |      ry| j                  |      S )
NchatIdr   FisGroupsenderIdfromTr   /)r   r7   rU   rs   ro   rF   r<   rB   
startswithr   r   r   )r   r   chat_id_rawis_grouprS   rj   r   s          r   _should_process_messagez-WhatsAppBehaviorMixin._should_process_message\  s.   $((8,23
 "";/88Iu-!G))'2DHHZ0JDHHV4DJKI--i8dhhx(.B/d88::--/488F#)r*002??3((.%%d+55d;;r   c                  	
 |s|S | j                  |      }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 ]  \  }}|j                  
 | d|      } t	              D ]  \  }}|j                  	 | 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 FENCEc                j    j                  | j                  d              t              dz
   dS Nr   rM    r   groupr)   )m	_FENCE_PHfencess    r   _save_fencez9WhatsAppBehaviorMixin.format_message.<locals>._save_fence  s1    MM!''!*%[Vq 166r   z```[\s\S]*?```z CODEc                j    j                  | j                  d              t              dz
   dS r   r   )r   _CODE_PHcodess    r   
_save_codez8WhatsAppBehaviorMixin.format_message.<locals>._save_code  s1    LL$ZE
Q/t44r   z	`[^`\n]+`z.(?<!\*)\*(?!\s|\*)([^*\n]*?\S[^*\n]*?)\*(?!\*)z_\1_z\*\*(.+?)\*\*z*\1*z	__(.+?)__z	~~(.+?)~~z~\1~c                4   | j                  d      j                         }t        |      dkD  rf|j                  d      rU|j	                  d      rD|dd j                         }t        |      dkD  r#|j                  d      r|j	                  d      rDd| dS )NrM   r]   )r   rB   r)   r   rR   )r   inners     r   _header_to_boldz=WhatsAppBehaviorMixin.format_message.<locals>._header_to_bold  s    GGAJ$$&Ee*q.U%5%5c%:u~~c?Ra))+ e*q.U%5%5c%:u~~c?RugQ<r   z^#{1,6}\s+(.+)$)flagsz\[([^\]]+)\]\(([^)]+)\)z\1 (\2)r   )r   zre.Matchreturnr   )r   r   r   	MULTILINE	enumerater$   )r   r   r   resultr   r   ifencecoder   r   r   r   s            @@@@r   format_messagez$WhatsAppBehaviorMixin.format_message  sd    N..w7  		7 );@ 	5 j&9 =
 ('6:gv6gv6	  r||

 2JG "&) 	BHAu^^yk!D$95AF	B ' 	@GAt^^xj4$8$?F	@ r   N)r   r   r   r   )r   r:   )r   r   )r   r	   )r   set[str])rN   zOptional[str]r   r   )rS   r   r   r:   )r_   r   r   r:   )rj   r   r   r:   )r   Dict[str, Any]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<   rF   staticmethodrI   rP   rU   rY   rc   rk   ro   rs   r   r   r   r   r   r   r   r   r   r   r   r   r   ,   s    #" \#\#-2::.K#L 'RZZ(^_< <  
)"?
N N N    (Y
 $ $L
,\F(O' <FJr   r   c                    ddl } ddlm} ddlm}  |t
              j                         j                  d   dz  dz  } |       }|dz  dz  }	 |dz  }|j                          |j                          d	}|r|S |j                         r|S 	 |j                  j                  d	d	       | j                  ||d
       |S # t        t        f$ r d
}Y ]w xY w# 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)Path)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copytreery   )r   _Pathr   install_bridgehermes_homehermes_home_bridge	test_fileinstall_writables           r   resolve_whatsapp_bridge_dirr     s    % 18_,,.66q9IEHYYN "#K$y03DD!"]2	    "!!	!!''t'D 	 	

 "!% _% ! !&  s$   'B? 2C ?CCC$#C$)r   r   )r   
__future__r   rw   loggingr!   r   typingr   r   r   	getLoggerr   r{   r   r   r   r   r   <module>r      sG   > #   	 	 & & 
		8	$] ]H.r   