
    jsj                    d    d Z ddlmZ ddlZddlmZ ddlmZ ddlm	Z	 ddl
mZmZ  G d d	      Zy)
a  User-authorization methods for ``GatewayRunner``.

Extracted from ``gateway/run.py`` as part of the god-file decomposition campaign
(``~/.hermes/plans/god-file-decomposition.md``, Phase 3 mechanical mixin lifts).
This mixin holds the inbound-message authorization cluster: whether a user/chat
is allowed to talk to the agent, the per-adapter DM policy, and the
unauthorized-DM behavior.

Behavior-neutral: every method is lifted verbatim from ``GatewayRunner``.
``self.*`` calls resolve unchanged via the MRO. Neutral dependencies import at
module top; the module-level ``logger`` is imported lazily inside the one method
that uses it (``from gateway.run import logger`` resolves at call time, when
``gateway.run`` is fully loaded) so this module never imports ``gateway.run`` at
import time -> no import cycle. The lazy import preserves the exact logger name
(``"gateway.run"``) so log records are unchanged.
    )annotationsN)Optional)Platform)SessionSource)expand_whatsapp_aliasesnormalize_whatsapp_identifierc                  L    e Zd ZdZd	dZd
dZd
dZ	 	 	 	 	 	 ddZddZd
dZ	y)GatewayAuthorizationMixinz6User/chat authorization methods for ``GatewayRunner``.c                |    |syt        | dd      }|sy|j                  |      }|yt        t        |dd            S )a  Whether the adapter for *platform* gates access at intake itself.

        Mirrors ``BasePlatformAdapter.enforces_own_access_policy``. Adapters
        such as WeCom, Weixin, Yuanbao, QQBot, and WhatsApp evaluate their
        documented ``dm_policy`` / ``group_policy`` / ``allow_from`` config before a
        message is dispatched to the gateway. The flag alone is NOT "already
        authorized": these adapters default to ``open``, which forwards every
        sender, so ``_is_user_authorized`` only trusts the adapter when its
        effective policy for the chat type is an actual ``allowlist`` restriction
        (see that method). Defaults to ``False`` when the adapter is unknown or
        doesn't expose the flag.
        FadaptersNenforces_own_access_policy)getattrgetbool)selfplatformr   adapters       R/home/cube/projects/richard/.worktrees/nutricoach-v140-impl/gateway/authz_mixin.py#_adapter_enforces_own_access_policyz=GatewayAuthorizationMixin._adapter_enforces_own_access_policy"   sK      4T2,,x(?GG%A5IJJ    c                   |syt        | dd      xs i }|j                  |      }|t        |dd      nd}|jt        | dd      }|'t        |d      r|j                  j                  |      nd}|rt        |dd      nd}t	        |t
              r|j                  d      }t        |xs d      j                         j                         S )	u  Best-effort read of an own-policy adapter's effective DM policy.

        Returns the lowercased ``dm_policy`` (``"open"`` / ``"allowlist"`` /
        ``"disabled"`` / ``"pairing"``) for *platform*, or ``""`` when unknown.
        Prefers the live adapter's resolved ``_dm_policy`` — which already folds
        in both ``config.extra`` and the ``<PLATFORM>_DM_POLICY`` env var (the
        env var is not always bridged back into ``config.extra``) — and falls
        back to ``config.extra`` for bare runners built without a live adapter.

        Used by ``_is_user_authorized`` to decide whether an own-policy adapter
        actually restricted DM senders to a configured allowlist (trustworthy)
        or merely forwarded everyone under ``dm_policy: open`` / for a pairing
        handshake (not authorization). "Reached the gateway" only carries an
        authorization signal in the ``allowlist`` case.
         r   N
_dm_policyconfig	platformsextra	dm_policy	r   r   hasattrr   
isinstancedictstrstriplowerr   r   r   r   policyr   platform_cfgr   s           r   _adapter_dm_policyz,GatewayAuthorizationMixin._adapter_dm_policy<   s      4T28b,,x(9@9L,5RV>T8T2F %'&+*F   $$X. 
 =IGL'48dE%&;/6<R &&(..00r   c                   |syt        | dd      xs i }|j                  |      }|t        |dd      nd}|jt        | dd      }|'t        |d      r|j                  j                  |      nd}|rt        |dd      nd}t	        |t
              r|j                  d      }t        |xs d      j                         j                         S )	a  Best-effort read of an own-policy adapter's effective group policy.

        Mirror of ``_adapter_dm_policy`` for group / forum / channel traffic:
        returns the lowercased ``group_policy`` (``"open"`` / ``"allowlist"`` /
        ``"disabled"``) for *platform*, or ``""`` when unknown. Prefers the live
        adapter's resolved ``_group_policy`` and falls back to ``config.extra``
        for bare runners built without a live adapter.

        Used by ``_is_user_authorized`` to decide whether an own-policy adapter
        restricted group senders to a configured allowlist (trustworthy) or
        forwarded the whole channel under ``group_policy: open`` (not
        authorization).
        r   r   N_group_policyr   r   r   group_policyr   r%   s           r   _adapter_group_policyz/GatewayAuthorizationMixin._adapter_group_policy]   s     4T28b,,x(<C<O/48UY>T8T2F %'&+*F   $$X. 
 =IGL'48dE%&>26<R &&(..00r   c                   |r|syt        | dd      xs i }|j                  |      }|t        |dd      nd}|jt        | dd      }|'t        |d      r|j                  j                  |      nd}|rt        |dd      nd}t	        |t
              r|j                  d      }t	        |t
              syt        |      }	|j                  |	      }
t	        |
t
              sa|	j                         }|j                         D ]>  \  }}t	        |t              s|j                         |k(  s+t	        |t
              s<|}
 n t	        |
t
              s|j                  d	      }
t	        |
t
              sy|
j                  d
      xs |
j                  d      }t	        |t              rt        |j                               S t	        |t        t        t        f      rt        d |D              S y)a  Whether a per-group sender allowlist gated this group message.

        WeCom supports ``groups.<group_id>.allow_from`` on top of the top-level
        ``group_policy``. A group may be open at the chat level while still
        restricting which senders inside that group can invoke Hermes. If such a
        message reached the gateway, the adapter already checked that sender
        allowlist, so it is a trustworthy intake decision rather than the
        fail-open ``group_policy: open`` case.
        Fr   N_groupsr   r   r   groups*
allow_from	allowFromc              3  N   K   | ]  }t        |      j                           y wN)r"   r#   ).0items     r   	<genexpr>zPGatewayAuthorizationMixin._adapter_group_has_sender_allowlist.<locals>.<genexpr>   s     B\Ts4y(\s   #%)r   r   r   r   r    r!   r"   r$   itemsr   r#   listtuplesetany)r   r   chat_idr   r   r/   r   r'   r   chat_id_str	group_cfgloweredkeyvaluesender_allows                  r   #_adapter_group_has_sender_allowlistz=GatewayAuthorizationMixin._adapter_group_has_sender_allowlist|   s    w4T28b,,x(6=6I)T2t>T8T2F %'&+*F   $$X. 
 =IGL'48dE%&8,&$''lJJ{+	)T*!'')G$lln
Uc3'CIIK7,BzRWY]G^ %I - )T*

3I)T* }}\2PimmK6PlC(**,--lT5#$67B\BBBr   c                   ddl m} |j                  t        j                  t        j
                  hv ry|j                  }|j                  dv r|j                  rt        j                  dt        j                  dij                  |j                  d      }|rut        j                  |d      j                         }|rO|j                  d      D ch c]"  }|j                         r|j                         $ }}d	|v s|j                  |v ry|sy
i t        j                  dt        j                   dt        j"                  dt        j$                  dt        j&                  dt        j(                  dt        j*                  dt        j,                  dt        j.                  dt        j0                  dt        j2                  dt        j4                  dt        j6                  dt        j8                  dt        j:                  dt        j<                  dt        j                  dt        j>                  di}t        j                  di}	t        j                  dt        j                  di}
i t        j                  dt        j                   dt        j"                  d t        j$                  d!t        j&                  d"t        j(                  d#t        j*                  d$t        j,                  d%t        j.                  d&t        j0                  d't        j2                  d(t        j4                  d)t        j6                  d*t        j8                  d+t        j:                  d,t        j<                  d-t        j                  d.t        j>                  d/i}t        j                   d0t        j4                  d1i}|j                  |vrx	 dd2l m!} |j                  |j                  jD                        }|rJ|jF                  r|jF                  ||j                  <   |jH                  r|jH                  ||j                  <   |j                  |j                  d      }|r't        j                  |d      jM                         d3v rytO        |d4d
      du rytO        |d5d
      rR|j                  |j                        }|r5t        j                  |d6      jM                         j                         d7v ry|j                  r|j                  jD                  nd}| jP                  jS                  ||      ryt        j                  |j                  |j                  d      d      j                         }d}d}|j                  d8v r|t        j                  |	j                  |j                  d      d      j                         }t        j                  |
j                  |j                  d      d      j                         }t        j                  d9d      j                         }|s|s|s|s| jU                  |j                        rq|j                  dv rB| jW                  |j                        }| jY                  |j                  |j                        ry| j[                  |j                        }|d:k(  ryt        j                  d;d      jM                         d3v S |rj|j                  d8v r\|j                  rP|j                  d      D ch c]#  }|j                         s|j                         % }}d	|v s|j                  |v ry|j                  t        j                  k(  r|r|j                  d8v r|j                  r|j                  d      D ch c]1  }|j                         j]                  d<      r|j                         3 }}|rMtO        | d=d
      s1|j_                  d>dja                  tc        |                   d| _2        |j                  |v rytg               }|r'|ji                  d? |j                  d      D               |r'|ji                  d@ |j                  d      D               |r'|ji                  dA |j                  d      D               d	|v ry|h}dB|v r#|jk                  |j                  dB      d          |j                  t        j"                  k(  rgtg               }|D ]  }|ji                  tm        |              |r|}|ji                  tm        |             to        |      }|r|jk                  |       |j                  @|j                  jD                  dCk(  r'|jp                  r|jk                  |jp                         ts        ||z        S c c}w # tJ        $ r Y w xY wc c}w c c}w )Dao  
        Check if a user is authorized to use the bot.
        
        Checks in order:
        1. Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true)
        2. Environment variable allowlists (TELEGRAM_ALLOWED_USERS, etc.)
        3. DM pairing approved list
        4. Global allow-all (GATEWAY_ALLOW_ALL_USERS=true)
        5. Default: deny
        r   )loggerT>   forumgroupchannelTELEGRAM_GROUP_ALLOWED_CHATSQQ_GROUP_ALLOWED_USERSr   ,r0   FTELEGRAM_ALLOWED_USERSDISCORD_ALLOWED_USERSWHATSAPP_ALLOWED_USERSWHATSAPP_CLOUD_ALLOWED_USERSSLACK_ALLOWED_USERSSIGNAL_ALLOWED_USERSEMAIL_ALLOWED_USERSSMS_ALLOWED_USERSMATTERMOST_ALLOWED_USERSMATRIX_ALLOWED_USERSDINGTALK_ALLOWED_USERSFEISHU_ALLOWED_USERSWECOM_ALLOWED_USERSWECOM_CALLBACK_ALLOWED_USERSWEIXIN_ALLOWED_USERSBLUEBUBBLES_ALLOWED_USERSQQ_ALLOWED_USERSYUANBAO_ALLOWED_USERSTELEGRAM_GROUP_ALLOWED_USERSTELEGRAM_ALLOW_ALL_USERSDISCORD_ALLOW_ALL_USERSWHATSAPP_ALLOW_ALL_USERSWHATSAPP_CLOUD_ALLOW_ALL_USERSSLACK_ALLOW_ALL_USERSSIGNAL_ALLOW_ALL_USERSEMAIL_ALLOW_ALL_USERSSMS_ALLOW_ALL_USERSMATTERMOST_ALLOW_ALL_USERSMATRIX_ALLOW_ALL_USERSDINGTALK_ALLOW_ALL_USERSFEISHU_ALLOW_ALL_USERSWECOM_ALLOW_ALL_USERSWECOM_CALLBACK_ALLOW_ALL_USERSWEIXIN_ALLOW_ALL_USERSBLUEBUBBLES_ALLOW_ALL_USERSQQ_ALLOW_ALL_USERSYUANBAO_ALLOW_ALL_USERSDISCORD_ALLOW_BOTSFEISHU_ALLOW_BOTS)platform_registry>   1yestruerole_authorizedis_botnone>   allmentions>   rG   rH   GATEWAY_ALLOWED_USERS	allowlistGATEWAY_ALLOW_ALL_USERS-#_warned_telegram_group_users_legacyu   TELEGRAM_GROUP_ALLOWED_USERS contains chat-ID-shaped values (%s). Treating them as chat IDs for backward compatibility. Move chat IDs to TELEGRAM_GROUP_ALLOWED_CHATS — the _USERS var is now for sender user IDs.c              3  ^   K   | ]%  }|j                         s|j                          ' y wr4   r#   r5   uids     r   r7   z@GatewayAuthorizationMixin._is_user_authorized.<locals>.<genexpr>  s%     c6SsWZW`W`Wbsyy{6S   --c              3  ^   K   | ]%  }|j                         s|j                          ' y wr4   r   r   s     r   r7   z@GatewayAuthorizationMixin._is_user_authorized.<locals>.<genexpr>  s%     e6UsY\YbYbYdsyy{6Ur   c              3  ^   K   | ]%  }|j                         s|j                          ' y wr4   r   r   s     r   r7   z@GatewayAuthorizationMixin._is_user_authorized.<locals>.<genexpr>  s%     a6QsUXU^U^U`syy{6Qr   @simplex):gateway.runrF   r   r   HOMEASSISTANTWEBHOOKuser_id	chat_typer=   TELEGRAMQQBOTr   osgetenvr#   splitDISCORDWHATSAPPWHATSAPP_CLOUDSLACKSIGNALEMAILSMS
MATTERMOSTMATRIXDINGTALKFEISHUWECOMWECOM_CALLBACKWEIXINBLUEBUBBLESYUANBAOgateway.platform_registryrt   rB   allowed_users_envallow_all_env	Exceptionr$   r   pairing_storeis_approvedr   r,   rD   r(   
startswithwarningjoinsortedr   r;   updateadd_expand_whatsapp_auth_aliases_normalize_whatsapp_identifier	user_namer   )r   sourcerF   r   chat_allowlist_envraw_chat_allowlistcidallowed_group_idsplatform_env_mapplatform_group_user_env_mapplatform_group_chat_env_mapplatform_allow_all_mapplatform_allow_bots_maprt   entryplatform_allow_all_varallow_bots_varplatform_nameplatform_allowlistgroup_user_allowlistgroup_chat_allowlistglobal_allowlisteffective_policyr=   vlegacy_chat_idsallowed_ids	check_idsnormalized_allowed_ids
allowed_idnormalized_user_ids                                  r   _is_user_authorizedz-GatewayAuthorizationMixin._is_user_authorized   s@	    	' ??x55x7G7GHH.. <<!!#A 8" c&//2&  "%'YY/A2%F%L%L%N"% $6#;#;C#@)#@C99; 		#@ & )
 //6>>EV3V#
7
5
 7
 ##%C	

 NN1
 OO3
 NN1
 LL-
 !;
 OO3
 7
 OO3
 NN1
 ##%C
 OO3
    "=!
" NN.#
$ 5%
* ='
# =NN4'
#"
9"
7"
 9"
 ##%E	"

 NN3"
 OO5"
 NN3"
 LL/"
 !="
 OO5"
 9"
 OO5"
 NN3"
 ##%E"
 OO5"
    "?!"
" NN0#"
$ 7%"
, 2OO0#
 ??"22	G)--foo.C.CD..<A<S<S(9**BGBUBU.v?
 "8!;!;FOOR!P!bii0F&K&Q&Q&SWk&k 6,e4<68U+488IN"))NF"C"I"I"K"Q"Q"SWj"j 28--b))-A  YY'7';';FOOR'PRTU[[]!!11#%99-H-L-LV__^`-ace#f#l#l#n #%99-H-L-LV__^`-ace#f#l#l#n 99%<bAGGI!*>G[dt0 77H##'DD'+'A'A&//'R$??  $'+'>'>v'O$#{2996;AACG[[[
  F$4$48J$Jv~~/C/I/I#/N!/NGRYR_R_Ra/N  ! ''6>>=N+N OOx000$  $66 .33C88A779'', 	8  
 t%JERNN6 !89 @DD<>>_4 ec6H6N6Ns6Scce6J6P6PQT6Ueea6F6L6LS6Qaa +I	'>MM'--,Q/0 ??h///%(U")
&--.KJ.WX *%4:7CD!?!H!01 OO'%%2  MM&**+I+,,[)T  \!$s+   'gA7g $g:g-6g 	ggc                   t        | dd      }|rYt        |d      rM|rKt        |d      r|j                  j                  |      nd}|r dt        |di       v r|j	                  |      S |r't        |d      r|j
                  dk7  r|j
                  S |r|rt        |d      r|j                  j                  |      }|rt        |dd      nd}t        |t              rEt        |j                  d      xs d	      j                         j                         }|d
k(  ry|dv ry|ri t        j                  dt        j                  dt        j                  dt        j                  dt        j                   dt        j"                  dt        j$                  dt        j&                  dt        j(                  dt        j*                  dt        j,                  dt        j.                  dt        j0                  dt        j2                  dt        j4                  dt        j6                  dt        j8                  d}t        j                  dt        j8                  di}t;        j<                  |j                  |d	      d	      j                         ry|j                  |d       D ](  }t;        j<                  |d	      j                         s( y t;        j<                  d!d	      j                         ryy)"u  Return how unauthorized DMs should be handled for a platform.

        Resolution order:
        1. Explicit per-platform ``unauthorized_dm_behavior`` in config — always wins.
        2. Explicit global ``unauthorized_dm_behavior`` in config — wins when no per-platform.
        3. When an allowlist (``PLATFORM_ALLOWED_USERS``,
           ``PLATFORM_GROUP_ALLOWED_USERS`` / ``PLATFORM_GROUP_ALLOWED_CHATS``,
           or ``GATEWAY_ALLOWED_USERS``) is configured, default to ``"ignore"`` —
           the allowlist signals that the owner has deliberately restricted
           access; spamming unknown contacts with pairing codes is both noisy
           and a potential info-leak. (#9337)
        4. No allowlist and no explicit config → ``"pair"`` (open-gateway default).
        r   Nget_unauthorized_dm_behaviorr   unauthorized_dm_behaviorr   pairr   r   pairing>   disabledr~   ignorerM   rN   rO   rP   rQ   rR   rS   rT   rU   rV   rW   rX   rY   rZ   r[   r\   r]   )r_   rJ   )rK    r}   )r   r   r   r   r   r   r    r!   r"   r#   r$   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   )	r   r   r   r'   r   r   r   platform_group_env_mapenv_keys	            r   _get_unauthorized_dm_behaviorz7GatewayAuthorizationMixin._get_unauthorized_dm_behavior  s    x. gf&DE(=DV[=Y6++//9_cL :glT[]_>` `::8DD gf&@A..&8666 76;#?!++//9L<HGL'48dE%&		+ 6 <"=CCEKKM		)! 99#
  !!#;   #:  !!#;  '')G	 
 #8  #9  #8  #6  ##%?  #9  !!#;  #9  #8  '')G  #9   $$&A! " #5# ( !! $  ;&" yy)--h;R@FFH155hC99Wb)//1# D 99,b1779r   N)r   Optional[Platform]returnr   )r   r   r   r"   )r   r   r=   zOptional[str]r   r   )r   r   r   r   )
__name__
__module____qualname____doc__r   r(   r,   rD   r   r   r   r   r   r
   r
      sE    @K41B1>2$2 2 
	2hU-nQr   r
   )r   
__future__r   r   typingr   gateway.configr   gateway.sessionr   gateway.whatsapp_identityr   r   r   r   r
   r   r   r   <module>r      s,   " # 	  # )y yr   