
    `gjIK                    j   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mZ 	 ddlmZ dZ	 ddlmZ dZ	 ddlZdZddlmZmZ dd	lmZmZmZmZ dd
lm Z m!Z!  ejD                  e#      Z$dZ%dZ&dZ'dZ(dZ)dZ*ddZ+ G d de      Z,y# e$ r dZdZY ew xY w# e$ r dZdZY kw xY w# e$ r dZdZY sw xY w)a  WeCom callback-mode adapter for self-built enterprise applications.

Unlike the bot/websocket adapter in ``wecom.py``, this handles the standard
WeCom callback flow: WeCom POSTs encrypted XML to an HTTP endpoint, the
adapter decrypts it, queues the message for the agent, and immediately
acknowledges.  The agent's reply is delivered later via the proactive
``message/send`` API using an access-token.

Supports multiple self-built apps under one gateway instance, scoped by
``corp_id:user_id`` to avoid cross-corp collisions.
    )annotationsN)AnyDictListOptionalTF)web)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResult)WXBizMsgCryptWeComCryptoErrorz0.0.0.0i!  z/wecom/callbacki   i   i,  c                 .    t         xr t        xr t        S N)AIOHTTP_AVAILABLEHTTPX_AVAILABLEDEFUSEDXML_AVAILABLE     [/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/wecom/callback_adapter.py!check_wecom_callback_requirementsr   B   s    II5IIr   c                       e Zd Zd fdZedd       Zedd       ZddddZddZddZ		 	 d	 	 	 	 	 	 	 	 	 dd	Z
dd
ZddZddZddZddZddZ	 	 	 	 	 	 	 	 	 	 	 	 ddZd dZd!dZd"dZd#dZd#dZ xZS )$WecomCallbackAdapterc                   t         |   |t        j                         |j                  xs i }t        |j                  d      xs t              | _        t        |j                  d      xs t              | _        t        |j                  d      xs t              | _        | j                  |      | _        d | _        d | _        d | _        d | _        t)        j*                         | _        d | _        i | _        i | _        i | _        y )Nhostportpath)super__init__r	   WECOM_CALLBACKextrastrgetDEFAULT_HOST_hostintDEFAULT_PORT_portDEFAULT_PATH_path_normalize_apps_apps_runner_site_app_http_clientasyncioQueue_message_queue
_poll_task_seen_messages_user_app_map_access_tokens)selfconfigr#   	__class__s      r   r!   zWecomCallbackAdapter.__init__G   s    !8!89"6*:l;
6*:l;
6*:l;
+/+?+?+F
04,0
/3	9=;B==?2602-/9;r   c                    | r|  d| S |S )N:r   )corp_iduser_ids     r   _user_app_keyz"WecomCallbackAdapter._user_app_key\   s    )0'!G9%=g=r   c           	        | j                  d      }t        |t              r,|r*|D cg c]  }t        |t              st        |        c}S | j                  d      rv| j                  d      xs d| j                  dd      | j                  dd      t	        | j                  dd            | j                  dd      | j                  d	d      d
gS g S c c}w )Nappsr?   namedefault corp_secretagent_idtokenencoding_aes_key)rD   r?   rG   rH   rI   rJ   )r%   
isinstancelistdictr$   )r#   rC   apps      r   r-   z$WecomCallbackAdapter._normalize_apps`   s    yy dD!d)-G#C1FDIGG99Y "IIf-:$yyB7#(99]B#? #EIIj"$= >"YYw3(-		2Db(I	 	 	 Hs
   CCF)is_reconnectc          	     0  K   ~| j                   st        j                  d       yt               st        j                  d       y	 t	        j
                  t        j                  t        j                        5 }|j                  d       |j                  d| j                  f       d d d        t        j                  d| j                         y# 1 sw Y   *xY w# t        t        f$ r Y nw xY w	 ddlm} t!        j"                  d	 |       
      | _        t'        j(                  t*              | _        | j,                  j.                  j1                  d| j2                         | j,                  j.                  j1                  | j4                  | j6                         | j,                  j.                  j9                  | j4                  | j:                         t'        j<                  | j,                        | _        | j>                  jA                          d {  7   t'        jB                  | j>                  | jD                  | j                        | _#        | jF                  jI                          d {  7   tK        jL                  | jO                               | _(        | jS                          t        jU                  d| jD                  | j                  | j4                         | j                   D ]Z  }	 | jW                  |       d {  7   # tX        $ r1}t        j                  d|j[                  dd      |       Y d }~Td }~ww xY w y# tX        $ r1 | j]                          d {  7   t        j_                  d       Y yw xY ww)Nz+[WecomCallback] No callback apps configuredFz+[WecomCallback] aiohttp/httpx not installed   z	127.0.0.1z&[WecomCallback] Port %d already in user   )platform_httpx_limitsg      4@)timeoutlimits)client_max_sizez/healthz0[WecomCallback] HTTP server listening on %s:%s%sz=[WecomCallback] Initial token refresh failed for app '%s': %srD   rE   Tz[WecomCallback] Failed to start)0r.   loggerwarningr   _socketsocketAF_INETSOCK_STREAM
settimeoutconnectr*   errorConnectionRefusedErrorOSError%gateway.platforms._http_client_limitsrR   httpxAsyncClientr2   r   Application	_MAX_BODYr1   routeradd_get_handle_healthr,   _handle_verifyadd_post_handle_callback	AppRunnerr/   setupTCPSiter'   r0   startr3   create_task
_poll_loopr6   _mark_connectedinfo_refresh_access_token	Exceptionr%   _cleanup	exception)r:   rO   sockrR   rN   excs         r   r]   zWecomCallbackAdapter.connectv   s     zzNNHI02NNHI	1D1DE 8"k4::678 LLA4::N	8 8
 '0 		 	S % 1 1$G\G^ _D 	BDIII$$Y0C0CDII$$TZZ1D1DEII%%djj$2G2GH==3DL,,$$&&&T\\4::tzzJDJ**""$$$%11$//2CDDO  "KKB

DJJ

 zz 44S999  NNW	2C   	--/!!>?	s   AN2C 9/C((C NCC C/,N.C//N3D+M H!AM ;I><BM ?LLLM 	M#'M
M MM NN5M86NNNNc                R  K   d| _         | j                  r6| j                  j                          	 | j                   d {    d | _        | j                          d {    | j                          t        j                  d       y 7 I# t        j                  $ r Y \w xY w7 Ew)NFz[WecomCallback] Disconnected)	_runningr6   cancelr3   CancelledErrorrv   _mark_disconnectedrV   rs   r:   s    r   
disconnectzWecomCallbackAdapter.disconnect   s     ??OO""$oo%% #DOmmo!23 &))  	sF   .B'B  B
B B'B% *B'
B B"B'!B""B'c                  K   d | _         | j                  r)| j                  j                          d {    d | _        d | _        | j                  r*| j                  j                          d {    d | _        y y 7 I7 wr   )r0   r/   cleanupr1   r2   acloser   s    r   rv   zWecomCallbackAdapter._cleanup   sq     
<<,,&&(((DL	##**,,, $D  ) -s!   1BA=;B/A?0B?Bc           
     P  K   | j                  |      }d|v r|j                  dd      d   n|}	 |dt        t        |j	                  d      xs d            d|d d idd}t        d	      D ]  }| j                  |       d {   }	| j                  j                  d
|	 |       d {   }
|
j                         }|j	                  d      }|dv rL|dk(  rGt        j                  d|j	                  dd      |       | j                  j                  |d   d        |dk7  rt        dt        |            c S t        dt        |j	                  dd            |      c S  t        dd      S 7 7 # t        $ r }t        dt        |            cY d }~S d }~ww xY ww)Nr>   rQ   textrH   r   contenti   )tousermsgtypeagentidr   safe   z>https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=)jsonerrcode>     A  zD[WecomCallback] Token rejected for app '%s' (errcode=%s), refreshingrD   rE   F)successr^   TmsgidrF   )r   
message_idraw_responsezsend failed after token refresh)_resolve_app_for_chatsplitr(   r$   r%   range_get_access_tokenr2   postr   rV   rW   r9   popr   ru   )r:   chat_idr   reply_tometadatarN   r   payload_attemptrI   respdatar   ry   s                 r   sendzWecomCallbackAdapter.send   s     ((1-0G^sA&q)"	= !s377:#6#;!<="GETN3G "!H "44S99!..33TUZT[\  4   yy{((9-n,Q NN^	2G ''++CK>a<%e3t9EE! "488GR#89!% '0 e3TUU/ :.  	=e3s8<<	=sr   -F&AE: E6'E: ,E8-BE: >F&?(E: 'F&(E: 5F&6E: 8E: :	F#FF#F&F##F&c                R   | j                   j                  |      }|s^d|vrZ| j                   D cg c]  }|j                  d|       s| }}t        |      dk(  r| j                   j                  |d         }|r| j	                  |      nd}|xs | j
                  d   S c c}w )z>Pick the app associated with *chat_id*, falling back sensibly.r>   rQ   r   N)r8   r%   endswithlen_get_app_by_namer.   )r:   r   app_namekmatchingrN   s         r   r   z*WecomCallbackAdapter._resolve_app_for_chat   s    %%))'2Cw.#'#5#5SaayM9RSHS8}!--11(1+>19d##H-t#djjm#	 Ts   B$
B$c                   K   |ddS w)Ndm)rD   typer   )r:   r   s     r   get_chat_infoz"WecomCallbackAdapter.get_chat_info  s     ..s   c                :   K   t        j                  ddd      S w)Nokwecom_callback)statusplatform)r   json_response)r:   requests     r   rh   z#WecomCallbackAdapter._handle_health
  s       D>N!OPPs   c                  K   |j                   j                  dd      }|j                   j                  dd      }|j                   j                  dd      }|j                   j                  dd      }| j                  D ]@  }	 | j                  |      }|j	                  ||||      }t        j                  |d      c S  t        j                  dd	
      S # t        $ r Y ew xY ww)u2   GET endpoint — WeCom URL verification handshake.msg_signaturerF   	timestampnonceechostr
text/plainr   content_typei  zsignature verification failedr   r   )queryr%   r.   _crypt_for_app
verify_urlr   Responseru   )	r:   r   r   r   r   r   rN   cryptplains	            r   ri   z#WecomCallbackAdapter._handle_verify  s     ))/2>MM%%k26	!!'2.--##Ir2:: 	C++C0((	5'R||\JJ		 ||3-LMM  s*   B C(;C>C(	C%"C($C%%C(c                V  K   |j                   j                  dd      }|j                   j                  dd      }|j                   j                  dd      }|j                          d{   }t        |      t        kD  r6t
        j                  dt        |             t        j                  dd	      S |j                  d
d      }| j                  D ]  }	 | j                  |||||      }| j                  ||      }	|	|	j                  rt        j                         }
|	j                  | j                  v rs|
| j                  |	j                     z
  t         k  r9t
        j#                  d|	j                         t        j                  dd      c S | j                  |	j                  = |
| j                  |	j                  <   t        | j                        dkD  rB|
t         z
  }| j                  j%                         D ci c]  \  }}||kD  s|| c}}| _        |	j&                  rj|	j&                  j(                  rT| j+                  t-        |j                  d      xs d      |	j&                  j(                        }|d   | j.                  |<   | j0                  j3                  |	       d{    t        j                  dd      c S  t        j                  dd	      S 7 c c}}w 7 ># t4        $ r Y /t6        $ r t
        j9                  d       Y  Nw xY ww)u8   POST endpoint — receive an encrypted message callback.r   rF   r   r   Nu9   [WecomCallback] Payload too large (%d bytes) — rejectedi  zpayload too larger   utf-8replace)errorsz,[WecomCallback] Duplicate MsgId %s, skippingr   r   r   i  r?   rD   z&[WecomCallback] Error handling messagei  zinvalid callback payload)r   r%   readr   re   rV   rW   r   r   decoder.   _decrypt_request_build_eventr   timer7   MESSAGE_DEDUP_TTL_SECONDSdebugitemssourcer@   rA   r$   r8   r5   putr   ru   rw   )r:   r   r   r   r   
body_bytesbodyrN   	decryptedeventnowcutoffr   vmap_keys                  r   rk   z%WecomCallbackAdapter._handle_callback  s    ))/2>MM%%k26	!!'2. #<<>)
z?Y&NNVX[\fXgh<<s1DEE   ;:: #	C" 11}i	 ))#y9$ ''"iik ++t/B/BB"T%8%89I9I%JJMff &-[]b]m]m n'*||Q]'^ ^ $ 3 3E4D4D E@C++E,<,<=t223d:%(+D%DFDHDWDWD]D]D_2nDAqcdgmcm1a42nD/||(<(<"&"4"4	 2 8b95<<;O;O# 7:&k**73--11%888 ||NN=#	H ||3-GHHU *4 3o 9 $    !IJsv   A(L)*K.+A/L)B<K9L)A.K9K1K1BK98K79K9L)1K99	L&L)L&"L)%L&&L)c                h  K   	 | j                   j                          d{   }	 t        j                  | j	                  |            }| j
                  j                  |       |j                  | j
                  j                         7 j# t        $ r t        j                  d       Y #w xY ww)z;Drain the message queue and dispatch to the gateway runner.Nz'[WecomCallback] Failed to enqueue event)r5   r%   r3   rp   handle_message_background_tasksaddadd_done_callbackdiscardru   rV   rw   )r:   r   tasks      r   rq   zWecomCallbackAdapter._poll_loopO  s     --1133EL**4+>+>u+EF&&**40&&t'='='E'EF 3
  L  !JKLs3   B2BB2A$B B2B/,B2.B//B2c                    t        j                  |      }|j                  dd      }| j                  |      }|j	                  ||||      j                  d      S )NEncryptrF   rE   r   )ET
fromstringfindtextr   decryptr   )	r:   rN   r   r   r   r   rootencryptr   s	            r   r   z%WecomCallbackAdapter._decrypt_request^  sT     }}T"--	2-6##C(}}]IugFMMgVVr   c                x   t        j                  |      }|j                  d      xs dj                         }|dk(  r(|j                  d      xs dj                         }|dv ry |dvry |j                  dd      }|j                  d	|j	                  d
d            }| j                  ||      }|j                  dd      j                         }	|	s|dk(  rd}	|j                  d      xs | d|j                  dd       }
| j                  ||d||      }t        |	t        j                  |||
      S )NMsgTyperF   r   Event>   	subscribeenter_agent>   r   r   FromUserNamer   
ToUserNamer?   Contentz/startMsgIdr>   
CreateTime0r   )r   	chat_name	chat_typer@   	user_name)r   message_typer   raw_messager   )r   r   r   lowerr%   rA   stripbuild_sourcer   r   TEXT)r:   rN   xml_textr   msg_type
event_namer@   r?   scoped_chat_idr   msg_idr   s               r   r   z!WecomCallbackAdapter._build_eventg  sP   }}X&MM),299;w--06B==?J99,,---;--cggi6L-M++GW=--	2-6<<>8w.GMM'" G!DMM,MDEF 	 """ # 
 $)) 
 	
r   c           	         t        t        |j                  d      xs d      t        |j                  d      xs d      t        |j                  d      xs d            S )NrI   rF   rJ   r?   )rI   rJ   
receive_id)r   r$   r%   )r:   rN   s     r   r   z#WecomCallbackAdapter._crypt_for_app  sR    cggg&,"- );!<!BC3779-34
 	
r   c                \    |sy | j                   D ]  }|j                  d      |k(  s|c S  y )NrD   )r.   r%   )r:   rD   rN   s      r   r   z%WecomCallbackAdapter._get_app_by_name  s5    :: 	Cwwv$&
	 r   c                   K   | j                   j                  |d         }t        j                         }|r|j                  dd      |dz   kD  r|d   S | j                  |       d {   S 7 w)NrD   
expires_atr   <   rI   )r9   r%   r   rt   )r:   rN   cachedr   s       r   r   z&WecomCallbackAdapter._get_access_token  se     $$((V5iikfjjq1C"H<'?"//4444s   A&A/(A-)A/c                  K   | j                   j                  d|j                  d      |j                  d      d       d {   }|j                         }|j                  d      dk7  rt        d|       |d	   }t	        |j                  d
t
                    }|t        j                         |z   d| j                  |d   <   t        j                  d|j                  dd      |j                  dd      |       |S 7 w)Nz,https://qyapi.weixin.qq.com/cgi-bin/gettokenr?   rG   )corpid
corpsecret)paramsr   r   zWeCom token refresh failed: access_token
expires_in)rI   r  rD   zF[WecomCallback] Token refreshed for app '%s' (corp=%s), expires in %ssrE   rF   )
r2   r%   r   RuntimeErrorr(   ACCESS_TOKEN_TTL_SECONDSr   r9   rV   rs   )r:   rN   r   r   rI   r  s         r   rt   z*WecomCallbackAdapter._refresh_access_token  s     &&**:''),!ggm4 + 
 
 yy{88I!#!=dVDEE^$,0HIJ
))+
2,
CK( 	TGGFI&GGIr"		
 -
s   ADDB?D)r;   r
   )r?   r$   r@   r$   returnr$   )r#   Dict[str, Any]r  zList[Dict[str, Any]])rO   boolr  r  )r  None)NN)
r   r$   r   r$   r   Optional[str]r   Optional[Dict[str, Any]]r  r   )r   r$   r  r  )r   zweb.Requestr  zweb.Response)rN   r  r   r$   r   r$   r   r$   r   r$   r  r$   )rN   r  r   r$   r  zOptional[MessageEvent])rN   r  r  r   )rD   r  r  r  )rN   r  r  r$   )__name__
__module____qualname__r!   staticmethodrA   r-   r]   r   rv   r   r   r   rh   ri   rk   rq   r   r   r   r   r   rt   __classcell__)r<   s   @r   r   r   F   s    <* > >  * 5: 8t4%$ #'-1+=+= +=  	+=
 ++= 
+=Z	$/QN1If	LW!W),WW'*W36W 
W"
H
5r   r   )r  r  )-__doc__
__future__r   r3   loggingrY   rX   r   typingr   r   r   r   defusedxml.ElementTreeElementTreer   r   ImportErroraiohttpr   r   rb   r   gateway.configr	   r
   gateway.platforms.baser   r   r   r   $plugins.platforms.wecom.wecom_cryptor   r   	getLoggerr  rV   r&   r)   r+   re   r  r   r   r   r   r   r   <module>r(     s   
 #     , ,
!'

O
 4 ] ] P			8	$ 
 	  Jv. vU  !	B !  
C  EOs3   B B B& 	BB	B#"B#&	B21B2