
    (GjI                    t   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n# e$ r dZdZY nw xY w	 ddlmZ dZn# e$ r dZdZY nw xY w	 ddlZdZn# e$ r dZdZY nw xY w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j"        e#          Z$dZ%dZ&dZ'dZ(dZ)dZ*ddZ+ G d de          Z,dS )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,  returnboolc                 ,    t           ot          ot          S N)AIOHTTP_AVAILABLEHTTPX_AVAILABLEDEFUSEDXML_AVAILABLE     N/home/rurouni/.hermes/hermes-agent/plugins/platforms/wecom/callback_adapter.py!check_wecom_callback_requirementsr   B   s    II5IIr   c                       e Zd Zd3 fdZed4d            Zed5d            Zd6dZd7dZd7dZ		 	 d8d9dZ
d:dZd:dZd;d Zd;d!Zd;d"Zd7d#Zd<d)Zd=d,Zd>d.Zd?d0Zd@d1Zd@d2Z xZS )AWecomCallbackAdapterconfigr
   c                Z   t                                          |t          j                   |j        pi }t          |                    d          pt                    | _        t          |                    d          pt                    | _        t          |                    d          pt                    | _        |                     |          | _        d | _        d | _        d | _        d | _        t)          j                    | _        d | _        i | _        i | _        i | _        d S )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)selfr   r&   	__class__s      r   r$   zWecomCallbackAdapter.__init__G   s    !8999"6**:l;;
6**:l;;
6**:l;;
+/+?+?+F+F
04,0
/3	9=;B=??2602-/9;r   corp_idr'   user_idr   c                    | r|  d| n|S )N:r   )r?   r@   s     r   _user_app_keyz"WecomCallbackAdapter._user_app_key\   s     )0='%%G%%%g=r   r&   Dict[str, Any]List[Dict[str, Any]]c           	        |                      d          }t          |t                    r|rd |D             S |                      d          r|                      d          pd|                      dd          |                      dd          t          |                      dd                    |                      d	d          |                      d
d          dgS g S )Nappsc                V    g | ]&}t          |t                    t          |          'S r   )
isinstancedict).0apps     r   
<listcomp>z8WecomCallbackAdapter._normalize_apps.<locals>.<listcomp>d   s-    GGG#C1F1FGDIIGGGr   r?   namedefault corp_secretagent_idtokenencoding_aes_key)rN   r?   rQ   rR   rS   rT   )r(   rI   listr'   )r&   rG   s     r   r0   z$WecomCallbackAdapter._normalize_apps`   s    yy  dD!! 	Hd 	HGGGGGG99Y 
	 "IIf--:$yyB77#(99]B#?#? #EIIj"$=$= > >"YYw33(-		2Db(I(I 	 	 	r   r   c           	       K   | j         st                              d           dS t                      st                              d           dS 	 t	          j        t          j        t          j                  5 }|                    d           |	                    d| j
        f           d d d            n# 1 swxY w Y   t                              d| j
                   dS # t          t          f$ r Y nw xY w	 ddlm} t!          j        d	 |            
          | _        t'          j        t*                    | _        | j        j                            d| j                   | j        j                            | j        | j                   | j        j                            | j        | j                   t'          j        | j                  | _        | j                                          d {V  t'          j!        | j        | j"        | j
                  | _#        | j#        $                                 d {V  tK          j&        | '                                          | _(        | )                                 t          *                    d| j"        | j
        | j                   | j         D ]e}	 | +                    |           d {V  # tX          $ r:}t                              d|-                    dd          |           Y d }~^d }~ww xY wdS # tX          $ r8 | .                                 d {V  t          /                    d           Y dS w xY 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': %srN   rO   Tz[WecomCallback] Failed to start)0r1   loggerwarningr   _socketsocketAF_INETSOCK_STREAM
settimeoutconnectr-   errorConnectionRefusedErrorOSError%gateway.platforms._http_client_limitsrX   httpxAsyncClientr5   r   Application	_MAX_BODYr4   routeradd_get_handle_healthr/   _handle_verifyadd_post_handle_callback	AppRunnerr2   setupTCPSiter*   r3   startr6   create_task
_poll_loopr9   _mark_connectedinfo_refresh_access_token	Exceptionr(   _cleanup	exception)r=   sockrX   rL   excs        r   rc   zWecomCallbackAdapter.connectv   s     z 	NNHIII5022 	NNHIII5	1DEE 8"""k4:67778 8 8 8 8 8 8 8 8 8 8 8 8 8 8 LLA4:NNN5&0 	 	 	D	 	SSSSSS % 1$G\G\G^G^ _ _ _D 	BBBDII$$Y0CDDDI$$TZ1DEEEI%%dj$2GHHH=33DL,$$&&&&&&&&&T\4:tzJJDJ*""$$$$$$$$$%1$//2C2CDDDO  """KKB
DJ
   z  44S9999999999    NNW	22C       
 4 	 	 	--//!!!!!!!>???55	ss   )C% :2B8,C% 8B<<C% ?B< #C% %C98C9=F;L 9KL 
L0LL LL >M M Nonec                >  K   d| _         | j        rD| j                                         	 | j         d {V  n# t          j        $ r Y nw xY wd | _        |                                  d {V  |                                  t                              d           d S )NFz[WecomCallback] Disconnected)	_runningr9   cancelr6   CancelledErrorr|   _mark_disconnectedr\   ry   r=   s    r   
disconnectzWecomCallbackAdapter.disconnect   s      ? 	#O""$$$o%%%%%%%%)   "DOmmoo!!!233333s   9 A
Ac                   K   d | _         | j        r&| j                                         d {V  d | _        d | _        | j        r(| j                                         d {V  d | _        d S d S r   )r3   r2   cleanupr4   r5   acloser   s    r   r|   zWecomCallbackAdapter._cleanup   s      
< 	 ,&&(((((((((DL	 	%#**,,,,,,,,, $D	% 	%r   Nchat_idcontentreply_toOptional[str]metadataOptional[Dict[str, Any]]r   c           
       K   |                      |          }d|v r|                    dd          d         n|}	 |dt          t          |                    d          pd                    d|d d         idd}t          d	          D ]!}|                     |           d {V }	| j                            d
|	 |           d {V }
|
	                                }|                    d          }|dv rX|dk    rRt                              d|                    dd          |           | j                            |d         d            |dk    r t          dt          |                    c S t          dt          |                    dd                    |          c S t          dd          S # t          $ r(}t          dt          |                    cY d }~S d }~ww xY w)NrB   rW   textrR   r   r   i   )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), refreshingrN   rO   F)successrd   TmsgidrP   )r   
message_idraw_responsezsend failed after token refresh)_resolve_app_for_chatsplitr+   r'   r(   range_get_access_tokenr5   postr   r\   r]   r<   popr   r{   )r=   r   r   r   r   rL   r   payload_attemptrS   respdatar   r   s                 r   sendzWecomCallbackAdapter.send   s\      ((11-0G^^sA&&q))"	= !s377:#6#6#;!<<=="GETEN3 G "!HH  "44S99999999!.33\UZ\\  4         yy{{((9--n,,Q NN^	22G   '++CK>>>a<<%e3t99EEEEEE! "488GR#8#899!%     
 e3TUUUU 	= 	= 	=e3s88<<<<<<<<<	=s*   D=F? 94F? .F? ?
G1	G,&G1,G1c                    | j                                       }|sJdvrFfd| j         D             }t          |          dk    r | j                             |d                   }|r|                     |          nd}|p| j        d         S )z>Pick the app associated with *chat_id*, falling back sensibly.rB   c                D    g | ]}|                     d            |S )rB   )endswith)rK   kr   s     r   rM   z>WecomCallbackAdapter._resolve_app_for_chat.<locals>.<listcomp>   s0    SSSaMMM9R9RSSSSr   rW   r   N)r;   r(   len_get_app_by_namer1   )r=   r   app_namematchingrL   s    `   r   r   z*WecomCallbackAdapter._resolve_app_for_chat   s    %))'22 	?Cw..SSSS4#5SSSH8}}!!-11(1+>>19Cd##H---t#djm#r   c                   K   |ddS )Ndm)rN   typer   )r=   r   s     r   get_chat_infoz"WecomCallbackAdapter.get_chat_info   s      ...r   requestweb.Requestweb.Responsec                4   K   t          j        ddd          S )Nokwecom_callback)statusplatform)r   json_response)r=   r   s     r   rn   z#WecomCallbackAdapter._handle_health  s        D>N!O!OPPPr   c                  K   |j                             dd          }|j                             dd          }|j                             dd          }|j                             dd          }| j        D ]W}	 |                     |          }|                    ||||          }t          j        |d          c S # t          $ r Y Tw xY wt          j        dd	
          S )u2   GET endpoint — WeCom URL verification handshake.msg_signaturerP   	timestampnonceechostr
text/plainr   content_typei  zsignature verification failedr   r   )queryr(   r1   _crypt_for_app
verify_urlr   Responser{   )	r=   r   r   r   r   r   rL   cryptplains	            r   ro   z#WecomCallbackAdapter._handle_verify  s      ))/2>>M%%k266	!!'2..-##Ir22: 	 	C++C00((	5'RR|\JJJJJJ   |3-LMMMMs   9AB>>
C
Cc                  K   |j                             dd          }|j                             dd          }|j                             dd          }|                                 d{V }t          |          t          k    r>t
                              dt          |                     t          j        dd	          S |	                    d
d          }| j
        D ]}	 |                     |||||          }|                     ||          }	|	k|	j        rt          j                    }
|	j        | j        v rc|
| j        |	j                 z
  t           k     r8t
                              d|	j                   t          j        dd          c S | j        |	j        = |
| j        |	j        <   t          | j                  dk    r4|
t           z
  fd| j                                        D             | _        |	j        r^|	j        j        rR|                     t-          |                    d          pd          |	j        j                  }|d         | j        |<   | j                            |	           d{V  t          j        dd          c S # t4          $ r Y t6          $ r t
                              d           Y  nw xY wt          j        dd	          S )u8   POST endpoint — receive an encrypted message callback.r   rP   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  c                (    i | ]\  }}|k    ||S r   r   )rK   r   vcutoffs      r   
<dictcomp>z9WecomCallbackAdapter._handle_callback.<locals>.<dictcomp>7  s+    2n2n2nDAqcdgmcmcm1acmcmcmr   r?   rN   z&[WecomCallback] Error handling messagei  zinvalid callback payload)r   r(   readr   rk   r\   r]   r   r   decoder1   _decrypt_request_build_eventr   timer:   MESSAGE_DEDUP_TTL_SECONDSdebugitemssourcer@   rC   r'   r;   r8   putr   r{   r}   )r=   r   r   r   r   
body_bytesbodyrL   	decryptedeventnowmap_keyr   s               @r   rq   z%WecomCallbackAdapter._handle_callback  s     ))/2>>M%%k266	!!'2.. #<<>>))))))
z??Y&&NNVX[\fXgXghhh<s1DEEEE   ;;: #	 #	C" 11}i 	 ))#y99$ ' o"ikk +t/BBB"T%89I%JJMfff &-[]b]m n n n'*|Q]'^'^'^ ^ ^ ^ $ 3E4D E@C+E,<=t233d::%(+D%DF2n2n2n2nDDWD]D]D_D_2n2n2nD/| B(< B"&"4"4	 2 2 8b995<;O# # 7:&k*73-11%888888888 |NNNNNN#        !IJJJ |3-GHHHHs    'B-JDJ
K)#KKc                `  K   	 | j                                          d{V }	 t          j        |                     |                    }| j                            |           |                    | j        j                   n*# t          $ r t                              d           Y nw xY w)z;Drain the message queue and dispatch to the gateway runner.TNz'[WecomCallback] Failed to enqueue event)r8   r(   r6   rv   handle_message_background_tasksaddadd_done_callbackdiscardr{   r\   r}   )r=   r   tasks      r   rw   zWecomCallbackAdapter._poll_loopI  s      	L-1133333333EL*4+>+>u+E+EFF&**4000&&t'='EFFFF L L L  !JKKKKKL	Ls   A B $B,+B,rL   r   r   r   r   c                    t          j        |          }|                    dd          }|                     |          }|                    ||||                              d          S )NEncryptrP   rO   r   )ET
fromstringfindtextr   decryptr   )	r=   rL   r   r   r   r   rootencryptr   s	            r   r   z%WecomCallbackAdapter._decrypt_requestX  sa     }T""--	2-66##C((}}]IugFFMMgVVVr   xml_textOptional[MessageEvent]c                   t          j        |          }|                    d          pd                                }|dk    r/|                    d          pd                                }|dv rd S |dvrd S |                    dd          }|                    d	|                    d
d                    }|                     ||          }|                    dd                                          }	|	s|dk    rd}	|                    d          p| d|                    dd           }
|                     ||d||          }t          |	t          j
        |||
          S )NMsgTyperP   r   Event>   	subscribeenter_agent>   r   r   FromUserNamer   
ToUserNamer?   Contentz/startMsgIdrB   
CreateTime0r   )r   	chat_name	chat_typer@   	user_name)r   message_typer   raw_messager   )r   r   r   lowerr(   rC   stripbuild_sourcer   r   TEXT)r=   rL   r   r   msg_type
event_namer@   r?   scoped_chat_idr   msg_idr   s               r   r   z!WecomCallbackAdapter._build_eventa  s   }X&&MM),,299;;w--006B==??J999t,,,4---;;--cggi6L6L-MM++GW==--	2-66<<>> 	8w..GMM'"" GFFDMM,MDDFF 	 """ # 
 
 $) 
 
 
 	
r   r   c           	         t          t          |                    d          pd          t          |                    d          pd          t          |                    d          pd                    S )NrS   rP   rT   r?   )rS   rT   
receive_id)r   r'   r(   )r=   rL   s     r   r   z#WecomCallbackAdapter._crypt_for_app  sn    cggg&&,"-- );!<!<!BCC3779--344
 
 
 	
r   rN   c                \    |sd S | j         D ]}|                    d          |k    r|c S  d S )NrN   )r1   r(   )r=   rN   rL   s      r   r   z%WecomCallbackAdapter._get_app_by_name  sH     	4: 	 	Cwwv$&&


 'tr   c                   K   | j                             |d                   }t          j                    }|r%|                    dd          |dz   k    r|d         S |                     |           d {V S )NrN   
expires_atr   <   rS   )r<   r(   r   rz   )r=   rL   cachedr   s       r   r   z&WecomCallbackAdapter._get_access_token  s      $((V55ikk 	#fjjq11C"H<<'?"//444444444r   c                V  K   | j                             d|                    d          |                    d          d           d {V }|                                }|                    d          dk    rt          d|           |d	         }t	          |                    d
t
                              }|t          j                    |z   d| j        |d         <   t          	                    d|                    dd          |                    dd          |           |S )Nz,https://qyapi.weixin.qq.com/cgi-bin/gettokenr?   rQ   )corpid
corpsecret)paramsr   r   zWeCom token refresh failed: access_token
expires_in)rS   r  rN   zF[WecomCallback] Token refreshed for app '%s' (corp=%s), expires in %ssrO   rP   )
r5   r(   r   RuntimeErrorr+   ACCESS_TOKEN_TTL_SECONDSr   r<   r\   ry   )r=   rL   r   r   rS   r  s         r   rz   z*WecomCallbackAdapter._refresh_access_token  s?     &**:''),,!ggm44  + 
 
 
 
 
 
 
 
 yy{{88I!##DdDDEEE^$,0HIIJJ
)++
2,
 ,
CK( 	TGGFI&&GGIr""		
 	
 	
 r   )r   r
   )r?   r'   r@   r'   r   r'   )r&   rD   r   rE   r   r   )r   r   )NN)
r   r'   r   r'   r   r   r   r   r   r   )r   r'   r   rD   )r   r   r   r   )rL   rD   r   r'   r   r'   r   r'   r   r'   r   r'   )rL   rD   r   r'   r   r   )rL   rD   r   r   )rN   r   r   r   )rL   rD   r   r'   )__name__
__module____qualname__r$   staticmethodrC   r0   rc   r   r|   r   r   r   rn   ro   rq   rw   r   r   r   r   r   rz   __classcell__)r>   s   @r   r   r   F   s       < < < < < <* > > > \>    \*2 2 2 2h4 4 4 4% % % %$ #'-1+= += += += +=Z	$ 	$ 	$ 	$/ / / /Q Q Q QN N N N1I 1I 1I 1If	L 	L 	L 	LW W W W"
 "
 "
 "
H
 
 
 
   5 5 5 5       r   r   r   )-__doc__
__future__r   r6   loggingr_   r^   r   typingr   r   r   r   defusedxml.ElementTreeElementTreer   r   ImportErroraiohttpr   r   rh   r   gateway.configr	   r
   gateway.platforms.baser   r   r   r   $plugins.platforms.wecom.wecom_cryptor   r   	getLoggerr!  r\   r)   r,   r.   rk   r  r   r   r   r   r   r   <module>r2     s%  
 
 # " " " " "        , , , , , , , , , , , ,
!'''''' ! ! !	B !   
CLLLOO   EOOO 4 3 3 3 3 3 3 3 ] ] ] ] ] ] ] ] ] ] ] ] P P P P P P P P		8	$	$ 
 	  J J J Jp p p p p. p p p p ps/   / 	;;A 	AAA 	A+*A+