
    i<b                      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ZddlZddl	Z	ddl
Z
ddlmZmZ ddlmZ ddlmZmZmZmZmZ ddlmZ 	 ddl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#m$Z$m%Z%m&Z& ddl'm(Z(  ej)        e*          Z+ G d de,          Z-ddl.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5m6Z6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZD ddlEmFZGmHZH ddZIddZJ G d de           ZKdS )uY  
QQ Bot platform adapter using the Official QQ Bot API (v2).

Connects to the QQ Bot WebSocket Gateway for inbound events and uses the
REST API (``api.sgroup.qq.com``) for outbound messages and media uploads.

Configuration in config.yaml:
    platforms:
      qq:
        enabled: true
        extra:
          app_id: "your-app-id"            # or QQ_APP_ID env var
          client_secret: "your-secret"     # or QQ_CLIENT_SECRET env var
          markdown_support: true           # enable QQ markdown (msg_type 2)
          dm_policy: "open"                # open | allowlist | disabled
          allow_from: ["openid_1"]
          group_policy: "open"             # open | allowlist | disabled
          group_allow_from: ["group_openid_1"]
          stt:                             # Voice-to-text config (optional)
            provider: "zai"                # zai (GLM-ASR), openai (Whisper), etc.
            baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4"
            apiKey: "your-stt-api-key"     # or set QQ_STT_API_KEY env var
            model: "glm-asr"               # glm-asr, whisper-1, etc.

    Voice transcription priority:
      1. QQ's built-in ``asr_refer_text`` (Tencent ASR — free, always tried first)
      2. Configured STT provider via ``stt`` config or ``QQ_STT_*`` env vars

Reference: https://bot.q.qq.com/wiki/develop/api-v2/
    )annotationsN)datetimetimezone)Path)AnyDictListOptionalTuple)urlparseTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResult_ssrf_redirect_guardcache_document_from_bytescache_image_from_bytes)strip_markdownc                  $     e Zd ZdZd fd	Z xZS )QQCloseErrorzRaised when QQ WebSocket closes with a specific code.

    Carries the close code and reason for proper handling in the reconnect loop.
     c                    |rt          |          nd | _        |rt          |          nd| _        t	                                          d| j         d| j         d           d S )Nr   zWebSocket closed (code=z	, reason=))intcodestrreasonsuper__init__)selfr   r   	__class__s      D/home/ubuntu/.hermes/hermes-agent/gateway/platforms/qqbot/adapter.pyr!   zQQCloseError.__init__T   se    !%/CIII4	%+3c&kkkU49UUt{UUUVVVVV    )r   )__name__
__module____qualname____doc__r!   __classcell__r#   s   @r$   r   r   N   sQ         
W W W W W W W W W Wr%   r   )API_BASE	TOKEN_URLGATEWAY_URL_PATHDEFAULT_API_TIMEOUTFILE_UPLOAD_TIMEOUTCONNECT_TIMEOUT_SECONDSRECONNECT_BACKOFFMAX_RECONNECT_ATTEMPTSRATE_LIMIT_DELAYQUICK_DISCONNECT_THRESHOLDMAX_QUICK_DISCONNECT_COUNTMAX_MESSAGE_LENGTHDEDUP_WINDOW_SECONDSDEDUP_MAX_SIZEMSG_TYPE_TEXTMSG_TYPE_MARKDOWNMSG_TYPE_MEDIAMSG_TYPE_INPUT_NOTIFYMEDIA_TYPE_IMAGEMEDIA_TYPE_VIDEOMEDIA_TYPE_VOICEMEDIA_TYPE_FILE)coerce_listbuild_user_agentreturnboolc                     t           ot          S )z/Check if QQ runtime dependencies are available.)AIOHTTP_AVAILABLEHTTPX_AVAILABLE r%   r$   check_qq_requirementsrJ   |   s    00r%   valuer   	List[str]c                     t          |           S )z0Coerce config values into a trimmed string list.)_coerce_list_impl)rK   s    r$   _coerce_listrO      s    U###r%   c                      e Zd ZdZdZeZdZdZedd            Z	dd
Z
d fdZedd            ZddZddZddZddZddZddZddZddZddZddZddZddZed             Zdd"Zdd%Zedd(            Zedd*            Zd fd-Zdd/Z dd3Z!dd4Z"dd5Z#dd6Z$edd:            Z%dd<Z&dd@Z'eddB            Z(ddDZ)dEdEdFddIZ*ddLZ+eddN            Z,eddO            Z-ddRZ.ddSZ/ddTZ0ddVZ1ddWZ2ddYZ3dEe4fdd_Z5	 	 	 	 dddfZ6dgZ7dhZ8ddiZ9	 	 dddnZ:	 dddoZ;	 dddqZ<	 dddsZ=	 ddduZ>	 dddvZ?	 	 	 dddyZ@	 	 ddd{ZA	 	 ddd}ZB	 	 dddZC	 	 	 dddZD	 	 	 dddZE	 dddZFdddZGddZHddZIedd            ZJddZKedd            ZLddZMddZNedd            ZOddZPddZQ xZRS )	QQAdapterzJQQ Bot adapter backed by the official QQ Bot WebSocket Gateway + REST API.F<   2   rD   r   c                6    t          | dd          }|rd| S dS )z>Log prefix including app_id for multi-instance disambiguation._app_idNzQQBot:QQBot)getattr)r"   app_ids     r$   _log_tagzQQAdapter._log_tag   s0     y$// 	%$F$$$wr%   r   Nonec                    | j                                         D ]8}|                                s"|                    t	          |                     9| j                                          dS )z"Fail all pending response futures.N)_pending_responsesvaluesdoneset_exceptionRuntimeErrorclear)r"   r   futs      r$   _fail_pendingzQQAdapter._fail_pending   sh    *1133 	8 	8C88:: 8!!,v"6"6777%%'''''r%   configr   c                *   t                                          |t          j                   |j        pi }t          |                    d          pt          j        dd                    	                                | _
        t          |                    d          pt          j        dd                    	                                | _        t          |                    dd                    | _        t          |                    dd	                    	                                                                | _        t!          |                    d
          p|                    d                    | _        t          |                    dd	                    	                                                                | _        t!          |                    d          p|                    d                    | _        d | _        d | _        d | _        d | _        d | _        d| _        d | _        d | _        i | _        i | _        i | _        i | _        i | _         d | _!        d| _"        tG          j$                    | _%        i | _&        d S )NrX   	QQ_APP_IDr   client_secretQQ_CLIENT_SECRETmarkdown_supportT	dm_policyopen
allow_from	allowFromgroup_policygroup_allow_fromgroupAllowFrom      >@        )'r    r!   r   QQBOTextrar   getosgetenvstriprU   _client_secretrE   _markdown_supportlower
_dm_policyrO   _allow_from_group_policy_group_allow_from_session_ws_http_client_listen_task_heartbeat_task_heartbeat_interval_session_id	_last_seq_chat_type_mapr\   _seen_messages_last_msg_id_typing_sent_at_access_token_token_expires_atasyncioLock_token_lock_upload_cache)r"   rd   rt   r#   s      r$   r!   zQQAdapter.__init__   s4   000"599X..L")K2L2LMMSSUU!IIo&&K")4F*K*K
 

%'' 	 "&eii0BD&I&I!J!J eiiV<<==CCEEKKMM'IIl##=uyy'='=
 
 !>6!B!BCCIIKKQQSS!-II())HUYY7G-H-H"
 "

 :>>B9=487;*. *.(,.0 >@02 -/13 -1(+"<>> 9;r%   c                    dS )NrV   rI   r"   s    r$   namezQQAdapter.name   s    wr%   rE   c                T  K   t           s=d}|                     d|d           t                              d| j        |           dS t
          s=d}|                     d|d           t                              d| j        |           dS | j        r| j        s=d	}|                     d
|d           t                              d| j        |           dS |                     d| j        d          sdS 	 ddl	m
} t          j        dddt          gi |                      | _        |                                  d{V  |                                  d{V }t                              d| j        |           |                     |           d{V  t'          j        |                                           | _        t'          j        |                                           | _        |                                  t                              d| j                   dS # t4          $ ry}d| }|                     d|d           t                              d| j        |d           |                                  d{V  |                                  Y d}~dS d}~ww xY w)z9Authenticate, obtain gateway URL, and open the WebSocket.z(QQ startup failed: aiohttp not installedqq_missing_dependencyT	retryablez![%s] %s. Run: pip install aiohttpFz&QQ startup failed: httpx not installedz[%s] %s. Run: pip install httpxz>QQ startup failed: QQ_APP_ID and QQ_CLIENT_SECRET are requiredqq_missing_credentialsz[%s] %szqqbot-appidzQQBot app IDr   )platform_httpx_limitsrq   response)timeoutfollow_redirectsevent_hookslimitsNz[%s] Gateway URL: %sz[%s] ConnectedzQQ startup failed: qq_connect_error)exc_info)rG   _set_fatal_errorloggerwarningrY   rH   rU   ry   _acquire_platform_lock%gateway.platforms._http_client_limitsr   httpxAsyncClientr   r   _ensure_token_get_gateway_urlinfo_open_wsr   create_task_listen_loopr   _heartbeat_loopr   _mark_connected	Exceptionerror_cleanup_release_platform_lock)r"   messager   gateway_urlexcs        r$   connectzQQAdapter.connect   s       	@G!!"97d!SSSNN>wWWW5 	>G!!"97d!SSSNN<dmWUUU5| 	4#6 	VG!!":Gt!TTTNN9dmW===5 **=$,WW 	5!	 TSSSSS % 1!%'*>)?@,,..	! ! !D $$&&&&&&&&& !% 5 5 7 7777777KKK.{KKK --,,,,,,,,, !( 3D4E4E4G4G H HD#*#6t7K7K7M7M#N#ND   """KK($-8884 	 	 	1C11G!!"4g!NNNLLDM7TLJJJ--//!!!!!!!'')))55555	s   5D-H$ $
J'.A.J""J'c                  K   d| _         |                                  | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        |                                  d{V  |                                  t          
                    d| j                   dS )z)Close all connections and stop listeners.FNz[%s] Disconnected)_running_mark_disconnectedr   cancelr   CancelledErrorr   r   r   r   r   rY   r   s    r$   
disconnectzQQAdapter.disconnect  sJ     !!! 	%$$&&&''''''''')    $D 	( '')))*********)   #'D mmoo##%%%'77777s#   A AA
B B*)B*c                  K   | j         r+| j         j        s| j                                          d{V  d| _         | j        r+| j        j        s| j                                         d{V  d| _        | j        r&| j                                         d{V  d| _        | j                                        D ]8}|                                s"|	                    t          d                     9| j                                         dS )z*Close WebSocket, HTTP session, and client.NDisconnected)r   closedcloser   r   acloser\   r]   r^   r_   r`   ra   )r"   rb   s     r$   r   zQQAdapter._cleanup1  s.     8 	#DHO 	#(.."""""""""= 	(!5 	(-%%''''''''' 	%#**,,,,,,,,, $D *1133 	@ 	@C88:: @!!,~">">???%%'''''r%   c                  K   | j         r&t          j                    | j        dz
  k     r| j         S | j        4 d{V  | j         r8t          j                    | j        dz
  k     r| j         cddd          d{V  S 	 | j                            t          | j        | j        dt                     d{V }|
                                 |                                }n%# t          $ r}t          d|           |d}~ww xY w|                    d          }|st          d|           t          |                    dd	                    }|| _         t          j                    |z   | _        t                               d
| j        |           | j         cddd          d{V  S # 1 d{V swxY w Y   dS )zFReturn a valid access token, refreshing if needed (with singleflight).rR   N)appIdclientSecret)jsonr   z#Failed to get QQ Bot access token: access_tokenz,QQ Bot token response missing access_token: 
expires_ini   z+[%s] Access token refreshed, expires in %ds)r   timer   r   r   postr-   rU   ry   r/   raise_for_statusr   r   r`   ru   r   r   r   rY   )r"   respdatar   tokenr   s         r$   r   zQQAdapter._ensure_tokenI  s      	&$)++0F0K"K"K%%# 	& 	& 	& 	& 	& 	& 	& 	&! *dikkD4JR4O&O&O)	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&
	Y!.33#'<ATUU/ 4        
 %%'''yy{{ Y Y Y"#N#N#NOOUXXY HH^,,E "I4II   TXXlD99::J!&D%)Y[[:%=D"KK=t}j   %9	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&s7   -F.>A"C! F.!
D+C>>DBF..
F8;F8c                  K   |                                   d{V }	 | j                            t           t           d| t                      dt                     d{V }|                                 |                                }n%# t          $ r}t          d|           |d}~ww xY w|                    d          }|st          d|           |S )z2Fetch the WebSocket gateway URL from the REST API.NQQBot )Authorization
User-Agentheadersr   z"Failed to get QQ Bot gateway URL: urlz%QQ Bot gateway response missing url: )r   r   ru   r,   r.   rC   r/   r   r   r   r`   )r"   r   r   r   r   r   s         r$   r   zQQAdapter._get_gateway_urll  s2     ((********	T*../-//%5e%5%5"2"4"4  , /        D !!###99;;DD 	T 	T 	TICIIJJPSS	T hhuoo 	OMtMMNNN
s   A0B 
B1B,,B1r   c                  K   | j         r+| j         j        s| j                                          d{V  d| _         | j        r+| j        j        s| j                                         d{V  d| _        t	          j                    | _        | j                            |dt                      it                     d{V | _         t          
                    d| j        |           dS )z2Open a WebSocket connection to the QQ Bot gateway.Nr   r   z[%s] WebSocket connected to %s)r   r   r   r   aiohttpClientSession
ws_connectrC   r1   r   r   rY   )r"   r   s     r$   r   zQQAdapter._open_ws  s      8 	#DHO 	#(.."""""""""= 	(!5 	(-%%'''''''''-//11.00 , 2 
 
 
 
 
 
 
 
 	4dm[QQQQQr%   c                  K   d}d}d}| j         r	 t          j                    }|                                  d{V  d}d}n}# t          j        $ r Y dS t          $ r}| j         sY d}~dS |j        }t          	                    d| j
        ||j                   t          j                    |z
  }|t          k     rw|dk    rq|dz  }t                              d| j
        ||           |t          k    r>t                              d| j
                   |                     dd	d
           Y d}~dS nd}|                                  |                     d           |dv rO|dk    rdnd}t                              d| j
        |           |                     d| d| d           Y d}~dS |dk    rt                              d| j
        t&                     |t(          k    rY d}~dS t	          j        t&                     d{V  |                     |           d{V rd}d}n|dz  }Y d}~0|dk    r.t                              d| j
                   d| _        d| _        |dv r/t                              d| j
        |           d| _        d| _        |                     |           d{V rd}d}n6|dz  }|t(          k    r&t                              d| j
                   Y d}~dS Y d}~nd}~wt6          $ r}| j         sY d}~dS t          	                    d| j
        |           |                                  |                     d           |t(          k    r&t                              d| j
                   Y d}~dS |                     |           d{V rd}d}n|dz  }Y d}~nd}~ww xY w| j         dS dS )u  Read WebSocket events and reconnect on errors.

        Close code handling follows the OpenClaw qqbot reference implementation:
          4004 → invalid token, refresh and reconnect
          4006/4007/4009 → session invalid, clear session and re-identify
          4008 → rate limited, back off 60s
          4914 → bot offline/sandbox, stop reconnecting
          4915 → bot banned, stop reconnecting
        r   rr   Nz([%s] WebSocket closed: code=%s reason=%s   z([%s] Quick disconnect (%.1fs), count: %dzf[%s] Too many quick disconnects. Check: 1) AppID/Secret correct 2) Bot permissions on QQ Open Platformqq_quick_disconnectu4   Too many quick disconnects — check bot permissionsTr   zConnection closed)2  i3  r   zoffline/sandbox-onlybannedz'[%s] Bot is %s. Check QQ Open Platform.qq_zBot is Fi  z%[%s] Rate limited (4008), waiting %dsi  z5[%s] Invalid token (4004), will refresh and reconnect)i  i  i  i$  i%  i&  i'  i(  i)  i*  i+  i,  i-  i.  i/  i0  i1  z9[%s] Session error (%d), clearing session for re-identifyz2[%s] Max reconnect attempts reached (QQCloseError)z[%s] WebSocket error: %szConnection interruptedz#[%s] Max reconnect attempts reached)r   r   	monotonic_read_eventsr   r   r   r   r   r   rY   r   r5   r   r6   r   r   r   rc   r4   r3   sleep
_reconnectr   r   r   r   r   )r"   backoff_idxconnect_timequick_disconnect_countr   r   durationdescs           r$   r   zQQAdapter._listen_loop  s      !"m I	%H%#~//''))))))))))*&&)    p p p} FFFFFx>MJ	    >++l:888\A=M=M*a/*KKB .	   .1KKKd M  
 --1R&* .   
  L ./*'')))""#6777 <''59T\\11xDLLA4=RV   ))$d&6&6&6% *    FFFFF 4<<KK?(  
 #&<<<!-(8999999999!__[99999999 )&'12..#q(HHHH 4<<KKO   *.D&-0D*    & KKS  
 (,D$%)DN55555555 "#K-.**1$K"&<<<%Y[_[hiii % % %} FFFFF94=#NNN'')))""#;<<<"888LL!FVVVFFFFF55555555 %"#K-.**1$K%u m I	% I	% I	% I	% I	%sX   1A N?
N?!K;.B?K;3A9K;27K;/AK;9B7K;;N?N:A5N:%N::N?r   r   c                p  K   t           t          |t          t                     dz
                     }t                              d| j        ||dz              t          j        |           d{V  d| _        	 | 	                                 d{V  | 
                                 d{V }|                     |           d{V  |                                  t                              d| j                   dS # t          $ r,}t                              d| j        |           Y d}~dS d}~ww xY w)	z<Attempt to reconnect the WebSocket. Returns True on success.r   z([%s] Reconnecting in %ds (attempt %d)...Nrq   z[%s] ReconnectedTz[%s] Reconnect failed: %sF)r2   minlenr   r   rY   r   r   r   r   r   r   r   r   r   )r"   r   delayr   r   s        r$   r   zQQAdapter._reconnect3  s_     !#k37H3I3IA3M"N"NO6M!O		
 	
 	
 mE"""""""""#' 		$$&&&&&&&&& $ 5 5 7 7777777K--,,,,,,,,,  """KK*DM:::4 	 	 	NN6sKKK55555	s   :BC? ?
D5	!D00D5c                  K   | j         st          d          | j        r#| j         r| j         j        s| j                                          d{V }|j        t          j        j        k    r2| 	                    |j
                  }|r|                     |           n|j        t          j        j        fv rnl|j        t          j        j        k    rt          |j
        |j                  |j        t          j        j        t          j        j        fv rt          d          | j        r| j         r| j         j        dS dS dS dS dS dS )z.Read WebSocket frames until connection closes.zWebSocket not connectedNzWebSocket closed)r   r`   r   r   receivetyper   	WSMsgTypeTEXT_parse_jsonr   _dispatch_payloadPINGCLOSEr   rt   CLOSEDERROR)r"   msgpayloads      r$   r   zQQAdapter._read_eventsJ  sp     x 	:8999m 	7 	7 	7((********Cx7,111**3844 4**7333g/4666W.444"38SY777g/68I8OPPP"#5666 m 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7 	7r%   c                  K   	 | j         rt          j        | j                   d{V  | j        r| j        j        r:	 | j                            d| j        d           d{V  n8# t          $ r+}t          
                    d| j        |           Y d}~nd}~ww xY w| j         dS dS # t          j        $ r Y dS w xY w)zSend periodic heartbeats (QQ Gateway expects op 1 heartbeat with latest seq).

        The interval is set from the Hello (op 10) event's heartbeat_interval.
        QQ's default is ~41s; we send at 80% of the interval to stay safe.
        Nr   opdz[%s] Heartbeat failed: %s)r   r   r   r   r   r   	send_jsonr   r   r   debugrY   r   )r"   r   s     r$   r   zQQAdapter._heartbeat_loop]  s%     	- RmD$<=========x 48? R(,,ADN-K-KLLLLLLLLLL  R R RLL!<dmSQQQQQQQQR - R R R R R % 	 	 	DD	s:   :B+ (A( 'B+ (
B2!BB+ B
B+ +B>=B>c                  K   |                                   d{V }dd| dddgdddd	d
d}	 | j        rN| j        j        sB| j                            |           d{V  t                              d| j                   dS t                              d| j                   dS # t          $ r,}t          	                    d| j        |           Y d}~dS d}~ww xY w)ae  Send op 2 Identify to authenticate the WebSocket connection.

        After receiving op 10 Hello, the client must send op 2 Identify with
        the bot token and intents. On success the server replies with a
        READY dispatch event.

        Reference: https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/reference.html
        N   r   i  Br   r   macOSzhermes-agent)z$osz$browserz$device)r   intentsshard
propertiesr   z[%s] Identify sentz2[%s] Cannot send Identify: WebSocket not connectedz [%s] Failed to send Identify: %s)
r   r   r   r   r   r   rY   r   r   r   )r"   r   identify_payloadr   s       r$   _send_identifyzQQAdapter._send_identifyp  sI      ((********)%))
 Q" .-  
 
"		Qx  h(()9:::::::::0$-@@@@@H$-      	Q 	Q 	QLL;T]CPPPPPPPPP	Qs   AB(  B( (
C2!CCc                  K   |                                   d{V }dd| | j        | j        dd}	 | j        rZ| j        j        sN| j                            |           d{V  t                              d| j        | j        | j                   dS t          	                    d| j                   dS # t          $ r:}t                              d| j        |           d| _        d| _        Y d}~dS d}~ww xY w)	zSend op 6 Resume to re-authenticate after a reconnection.

        Reference: https://bot.q.qq.com/wiki/develop/api-v2/dev-prepare/interface-framework/reference.html
        N   r   )r   
session_idseqr   z([%s] Resume sent (session_id=%s, seq=%s)z0[%s] Cannot send Resume: WebSocket not connectedz[%s] Failed to send Resume: %s)r   r   r   r   r   r   r   r   rY   r   r   r   )r"   r   resume_payloadr   s       r$   _send_resumezQQAdapter._send_resume  sM     
 ((********)%))".~ 
 
	"x  h((888888888>M$N	     F      	" 	" 	"LL94=#NNN#D!DNNNNNNN		"s   AB7  B7 7
C;/C66C;c                v    	 t          j                    }|                    |           S # t          $ r Y dS w xY w)zSchedule a coroutine, silently skipping if no event loop is running.

        This avoids ``RuntimeError: no running event loop`` when tests call
        ``_dispatch_payload`` synchronously outside of ``asyncio.run()``.
        N)r   get_running_loopr   r`   )coroloops     r$   _create_taskzQQAdapter._create_task  sK    	+--D##D))) 	 	 	44	s   '* 
88r   Dict[str, Any]c                "   |                     d          }|                     d          }|                     d          }|                     d          }t          |t                    r| j        || j        k    r|| _        |dk    rt          |t                    r|ni }|                     dd          }|d	z  d
z  | _        t                              d| j        || j                   | j	        r/| j        (| 
                    |                                            n'| 
                    |                                            dS |dk    r|r|dk    r|                     |           nu|dk    r!t                              d| j                   nN|dv r)t          j        |                     ||                     n!t                              d| j        |           dS |dk    rdS t                              d| j        |           dS )zPRoute inbound WebSocket payloads (dispatch synchronously, spawn async handlers).r   tsr   N
   heartbeat_intervali0u  g     @@g?zB[%s] Hello received, heartbeat_interval=%dms (sending every %.1fs)r   READYRESUMEDz[%s] Session resumed)C2C_MESSAGE_CREATEGROUP_AT_MESSAGE_CREATEDIRECT_MESSAGE_CREATEGUILD_MESSAGE_CREATEGUILD_AT_MESSAGE_CREATEz[%s] Unhandled dispatch: %s   z[%s] Unknown op: %s)ru   
isinstancer   r   dictr   r   r   rY   r   r  r  r  _handle_readyr   r   r   _on_message)r"   r   r   r  r  r   d_datainterval_mss           r$   r   zQQAdapter._dispatch_payload  s   [[KKKKKKa 	4>#9Q=O=ODN 88$Q--5QQ2F **%95AAK'2V';c'AD$LLT(	    9DN$>!!$"3"3"5"56666!!$"5"5"7"7888F 77q7G||""1%%%%i2DMBBBB    #D$4$4Q$:$:;;;;:DM1MMMF 88F*DM2>>>>>r%   r   r   c                    t          |t                    rB|                    d          | _        t                              d| j        | j                   dS dS )u7   Handle the READY event — store session_id for resume.r  z[%s] Ready, session_id=%sN)r"  r#  ru   r   r   r   rY   )r"   r   s     r$   r$  zQQAdapter._handle_ready  sW    a 	V uu\22DKK3T]DDTUUUUU	V 	Vr%   rawOptional[Dict[str, Any]]c                    	 t          j        |           }n,# t          $ r t                              d|            Y d S w xY wt          |t                    r|nd S )Nz [QQBot] Failed to parse JSON: %r)r   loadsr   r   r   r"  r#  )r)  r   s     r$   r   zQQAdapter._parse_json  sh    	jooGG 	 	 	NN=sCCC44	 %Wd33=ww=s    %A A msg_idc                    t          t          j                              dz  }t          t          j                    j        dd         d          }||z  dz  S )z5Generate a message sequence number in 0..65535 range.i N      i   )r   r   uuiduuid4hex)r-  	time_partrands      r$   _next_msg_seqzQQAdapter._next_msg_seq  sM     	$$y0	4:<<#BQB',,D E))r%   eventr   c                   K   |j         r%|j        j        r|j         | j        |j        j        <   t	                                          |           d{V  dS )z:Cache the last message ID per chat, then delegate to base.N)
message_idsourcechat_idr   r    handle_message)r"   r7  r#   s     r$   r<  zQQAdapter.handle_message  se       	G 4 	G6;6FDel23gg$$U+++++++++++r%   
event_typec                P  K   t          |t                    sdS t          |                    dd                    }|r|                     |          r#t
                              d| j        |           dS t          |                    dd                    }t          |                    dd                                                    }t          |                    d          t                    r|                    d          ni }|dk    r!| 	                    |||||           d{V  dS |d	v r!| 
                    |||||           d{V  dS |d
v r!|                     |||||           d{V  dS |dk    r!|                     |||||           d{V  dS dS )z(Process an inbound QQ Bot message event.Nidr   z([%s] Duplicate or missing message id: %s	timestampcontentauthorr  )r  )r  r   r  )r"  r#  r   ru   _is_duplicater   r   rY   rx   _handle_c2c_message_handle_group_message_handle_guild_message_handle_dm_message)r"   r=  r   r-  r@  rA  rB  s          r$   r%  zQQAdapter._on_message  s     !T"" 	F QUU4__%% 	++F33 	LL:DM6   Fk2..//	aeeIr**++1133$.quuX$E$EMx2 ---**1fgvyQQQQQQQQQQQ777,,QSSSSSSSSSSSNNN,,QSSSSSSSSSSS222))!VWfiPPPPPPPPPPP 32r%   rA  rB  r@  c                  K   t          |                    dd                    }|sdS |                     |          sdS |}|                    d          }t                              d| j        ||r
|dd         nd|r)t          |t                    rt          |          nd dnd	           |rt          |t                    rt          |          D ]\  }	}
t          |
t                    rut                              d
| j        |	|
                    dd          t          |
                    dd                    dd         |
                    dd                     |                     |           d{V }|d         }|d         }|d         }|d         }|rEd                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}t                              d| j        t          |          t          |                     |                                s|sdS d| j        |<   t          |                     ||d          ||                     ||          |||||                     |                    }|                     |           d{V  dS )z%Handle a C2C (private) message event.user_openidr   Nattachmentsz1[%s] C2C message: id=%s content=%r attachments=%srS   r   z itemsrZ   z7[%s] attachment[%d]: content_type=%s url=%s filename=%scontent_typer   P   filename
image_urlsimage_media_typesvoice_transcriptsattachment_info


z*[%s] After processing: images=%d, voice=%dc2cdmr;  user_id	chat_typer:  textmessage_typeraw_messager9  
media_urlsmedia_typesr@  )r   ru   _is_dm_allowedr   r   rY   r"  listr   	enumerater#  _process_attachmentsjoinrx   r   r   build_source_detect_message_type_parse_qq_timestampr<  )r"   r   r-  rA  rB  r@  rI  rZ  attachments_raw_i_att
att_resultrN  rO  rP  rQ  voice_blockr7  s                     r$   rD  zQQAdapter._handle_c2c_message8  sR      &**]B7788 	F"";// 	F%%..?M#+GCRCLL #:ot+L+LS3'''RS[[[[
	
 
	
 
	
  
	z/4@@ 
	%o66 	 	DdD)) KKQ44DHHUB//00"5R00    44_EEEEEEEE
-
&':;&':;$%67  	))$566K9=V,33555;   	 ::<<%077999$  	8M
OO!""		
 	
 	
 zz|| 	J 	F+0K($$## %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r%   c                  K   t          |                    dd                    }|sdS |                     |t          |                    dd                              sdS |                     |          }|                     |                    d                     d{V }|d         }	|d         }
|d         }|d	         }|rEd
                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}|                                s|	sdS d| j        |<   t          | 	                    |t          |                    dd                    d          || 
                    |	|
          |||	|
|                     |                    }|                     |           d{V  dS )zHandle a group @-message event.group_openidr   Nmember_openidrJ  rN  rO  rP  rQ  rR  rS  grouprV  rY  )r   ru   _is_group_allowed_strip_at_mentionrb  rc  rx   r   r   rd  re  rf  r<  )r"   r   r-  rA  rB  r@  rm  rZ  rj  rN  rO  rP  rQ  rk  r7  s                  r$   rE  zQQAdapter._handle_group_message  s@      1554455 	F%%c&**_b"A"ABB
 
 	 F %%g..44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67  	))$566K9=V,33555;   	 ::<<%077999$  zz|| 	J 	F,3L)$$$FJJ;;<<! %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r%   c                b  K   t          |                    dd                    }|sdS t          |                    dd                    }t          |                    dd                    }|                     |p||          s$t                              d| j        ||           dS t          |                    d          t                    r|                    d          ni }	t          |	                    dd                    p"t          |                    d	d                    }
|}|                     |                    d
                     d{V }|d         }|d         }|d         }|d         }|rEd	                    |          }|
                                r|dz   |z   
                                n|}|r0|
                                r|dz   |z   
                                n|}|
                                s|sdS d| j        |<   t          |                     |t          |                    dd                    |
pdd          ||                     ||          |||||                     |                    }|                     |           d{V  dS )z%Handle a guild/channel message event.
channel_idr   Nguild_idr?  z5[%s] Guild message blocked by ACL: channel=%s user=%smembernickusernamerJ  rN  rO  rP  rQ  rR  rS  guildro  )r;  rW  	user_namerX  rY  )r   ru   rp  r   r   rY   r"  r#  rb  rc  rx   r   r   rd  re  rf  r<  )r"   r   r-  rA  rB  r@  rs  rt  	author_idru  rv  rZ  rj  rN  rO  rP  rQ  rk  r7  s                      r$   rF  zQQAdapter._handle_guild_message  s      |R0011
 	F
 quuZ,,--

4,,--	%%h&<*iHH 	LLGz9   F$.quuX$E$EMx26::fb))**Mc&**Z2L2L.M.M44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67 	))$566K9=V,33555;   	 ::<<%077999$  zz|| 	J 	F*1J'$$"FJJtR0011,$!	 %   22:?PQQ!)..y99
 
 
 !!%(((((((((((r%   c                  K   t          |                    dd                    }|sdS t          |                    dd                    }|                     |          s$t                              d| j        ||           dS |}|                     |                    d                     d{V }	|	d         }
|	d         }|	d	         }|	d
         }|rEd                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}|                                s|
sdS d| j	        |<   t          |                     |t          |                    dd                    d          ||                     |
|          |||
||                     |                    }|                     |           d{V  dS )z Handle a guild DM message event.rt  r   Nr?  z.[%s] Guild DM blocked by ACL: guild=%s user=%srJ  rN  rO  rP  rQ  rR  rS  rU  rV  rY  )r   ru   r_  r   r   rY   rb  rc  rx   r   r   rd  re  rf  r<  )r"   r   r-  rA  rB  r@  rt  rz  rZ  rj  rN  rO  rP  rQ  rk  r7  s                   r$   rG  zQQAdapter._handle_dm_message  sP      quuZ,,-- 	F
 

4,,--	""9-- 	LL@x   F44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67 	))$566K9=V,33555;   	 ::<<%077999$  zz|| 	J 	F(,H%$$ FJJtR0011 %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r%   r]  r`  r^  c                <   | st           j        S |st           j        S |r|d                                         nd}d|v sd|v sd|v rt           j        S d|v rt           j        S d|v sd|v rt           j        S t                              d	|           t           j        S )
z4Determine MessageType from attachment content types.r   r   audiovoicesilkvideoimagephotoz3Unknown media content_type '%s', defaulting to TEXT)r   r   PHOTOr{   VOICEVIDEOr   r   )r]  r^  
first_types      r$   re  zQQAdapter._detect_message_typeQ  s      	$## 	%$$/:B[^))+++
j  Gz$9$9Vz=Q=Q$$j  $$j  Gz$9$9$$A	
 	
 	
 r%   rJ  c           	     0  K   t          |t                    sg g g ddS g }g }g }g }|D ]}t          |t                    st          |                    dd                                                                                    }t          |                    dd                                                    }t          |                    dd                    }	|                    d          rd| }
n|r|}
nd}
t          	                    d| j
        ||
d	d
         |	           |                     ||	          r_t          |                    d          t                    r5t          |                    dd                                                    nd}t          |                    d          t                    r5t          |                    dd                                                    nd}|                     |
||	|pd	|pd	           d	{V }|r;|                    d|            t          	                    d| j
        |           <t                              d| j
        |
d	d                    |                    d           ||                    d          r	 |                     |
|           d	{V }|rLt           j                            |          r-|                    |           |                    |pd           n#|r!t                              d| j
        |           !# t&          $ r,}t          	                    d| j
        |           Y d	}~Rd	}~ww xY w	 |                     |
|           d	{V }|r|                    d|	p| d           # t&          $ r,}t          	                    d| j
        |           Y d	}~d	}~ww xY w|rd                    |          nd}||||dS )u  Process inbound attachments (all message types).

        Mirrors OpenClaw's ``processAttachments`` — handles images, voice, and
        other files uniformly.

        Returns a dict with:
        - image_urls: list[str]  — cached local image paths
        - image_media_types: list[str] — MIME types of cached images
        - voice_transcripts: list[str] — STT transcripts for voice messages
        - attachment_info: str — text description of non-image, non-voice attachments
        r   )rN  rO  rP  rQ  rK  r   rM  //https:z@[%s] Processing attachment: content_type=%s, url=%s, filename=%sNrL  asr_refer_textvoice_wav_urlr  r  z[Voice] z[%s] Voice transcript: %sz[%s] Voice STT failed for %srR   u   [Voice] [语音识别失败]image/z
image/jpegz)[%s] Cached image path does not exist: %sz[%s] Failed to cache image: %sz[Attachment: ]z#[%s] Failed to cache attachment: %srR  )r"  r`  r#  r   ru   rx   r{   
startswithr   r   rY   _is_voice_content_type_stt_voice_attachmentappendr   _download_and_cacherv   pathisfiler   rc  )r"   rJ  rN  rO  rP  other_attachmentsattcturl_rawrM  r   	asr_referr  
transcriptcached_pathr   rQ  s                    r$   rb  zQQAdapter._process_attachmentse  s      +t,, 	 %'%'#%	   !#
')')') G	\ G	\Cc4(( SWW^R00117799??AAB#''%,,--3355G377:r2233H!!$'' (w(( LLRCRC   **2x88 0\ "#''*:";";SAAC 0"5566<<>>>  "#''/":":C@@C4455;;===  $(#=#=#,#4"/"74 $> $ $      
  M%,,-D
-D-DEEELL!<dmZXXXXNN#A4=RUVYWYVYRZ[[[%,,-KLLLLx(( \W(,(@(@b(I(I"I"I"I"I"I"IK" rw~~k'B'B "))+666)001C|DDDD$ G M'  
 ! W W WLL!A4=RUVVVVVVVVW\(,(@(@b(I(I"I"I"I"I"I"IK" T)001RR1R1R1RSSS  \ \ \LL!FWZ[[[[[[[[\ ;LS$))$5666QS$!2!2.	
 
 	
s1   ;BM


N !M;;N 9N??
O5	!O00O5r   rK  Optional[str]c                  K   ddl m}  ||          st          d|dd                    | j        sdS 	 | j                            |d|                                            d{V }|                                 |j        }nB# t          $ r5}t          
                    d| j        |dd         |           Y d}~dS d}~ww xY w|                    d	          r&t          j        |          pd
}t          ||          S |dk    s|                    d          r|                     ||           d{V S t#          t%          |          j                  j        pd}t+          ||          S )z$Download a URL and cache it locally.r   is_safe_urlzBlocked unsafe URL: NrL  rq   )r   r   z[%s] Download failed for %s: %sr  z.jpgr~  audio/qq_attachment)tools.url_safetyr  
ValueErrorr   ru   _qq_media_headersr   rA  r   r   r   rY   r  	mimetypesguess_extensionr   _convert_audio_to_wavr   r   r  r   r   )	r"   r   rK  r  r   r   r   extrM  s	            r$   r  zQQAdapter._download_and_cache  s     000000{3 	@>CH>>???  	4	*....00 /        D
 !!###<DD 	 	 	LL14=#crc(C   44444		 ""8,, 		=+L99CVC)$444W$$(?(?(I(I$ 33D#>>>>>>>>>HSMM.//4GH,T8<<<s   AB	 	
C*CCrM  c                   |                                                                  }|                                                                 |dk    s|                    d          rdS d}t          fd|D                       rdS dS )z0Check if an attachment is a voice/audio message.r~  r  T)	.silk.amr.mp3.wav.ogg.m4a.aacz.speex.flacc              3  B   K   | ]}                     |          V  d S N)endswith).0r  fns     r$   	<genexpr>z3QQAdapter._is_voice_content_type.<locals>.<genexpr>  s/      ==Cr{{3======r%   F)rx   r{   r  any)rK  rM  r  _VOICE_EXTENSIONSr  s       @r$   r  z QQAdapter._is_voice_content_type  s     !!''))^^##%%==BMM(33=4

 ====+<===== 	4ur%   Dict[str, str]c                ,    | j         rdd| j          iS i S )zReturn Authorization headers for QQ multimedia CDN downloads.

        QQ's multimedia URLs (multimedia.nt.qq.com.cn) require the bot's
        access token in an Authorization header, otherwise the download
        returns a non-200 status.
        r   r   )r   r   s    r$   r  zQQAdapter._qq_media_headers  s,      	D#%Bd.@%B%BCC	r%   Nr  r  r  c          	       K   |r+t                               d| j        |dd                    |S |}d}|r>|                    d          rd| }|}d}t                               d| j                   d	d
lm}  ||          s%t                               d|dd                    dS 	 | j        s"t                               d| j                   dS |                                 }	t                               d| j        |dd         |t          |	                     | j        
                    |d|	d           d{V }
|
                                 |
j        }t                               d| j        t          |          |
j        
                    dd                     t          |          dk     r0t                               d| j        t          |                     dS |r~d	dl}|                    dd          5 }|                    |           |j        }ddd           n# 1 swxY w Y   t                               d| j        t          |                     nt                               d| j        |           |                     ||           d{V }|r!t)          |                                          s"t                               d| j                   dS t                               d| j        |           |                     |           d{V }	 t/          j        |           n# t2          $ r Y nw xY w|r*t                               d| j        |dd                    n t                               d| j                   |S # t4          j        t4          j        t:          f$ r?}t                               d| j        t=          |          j        |           Y d}~dS d}~ww xY w)u  Download a voice attachment, convert to wav, and transcribe.

        Priority:
        1. QQ's built-in ``asr_refer_text`` (Tencent's own ASR — free, no API call).
        2. Self-hosted STT on ``voice_wav_url`` (pre-converted WAV from QQ, avoids SILK decoding).
        3. Self-hosted STT on the original attachment URL (requires SILK→WAV conversion).

        Returns the transcript text, or None on failure.
        z%[%s] STT: using QQ asr_refer_text: %rNd   Fr  r  Tz1[%s] STT: using voice_wav_url (pre-converted WAV)r   r  z[QQ] STT blocked unsafe URL: %srL  z[%s] STT: no HTTP clientz<[%s] STT: downloading voice from %s (pre_wav=%s, headers=%s)rq   )r   r   r   z.[%s] STT: downloaded %d bytes, content_type=%szcontent-typeunknownr  z8[%s] STT: downloaded data too small (%d bytes), skippingr  suffixdeletez5[%s] STT: using pre-converted WAV directly (%d bytes)z([%s] STT: converting to wav, filename=%rz.[%s] STT: ffmpeg conversion produced no outputz[%s] STT: calling ASR on %sz[%s] STT success: %rz'[%s] STT: ASR returned empty transcriptz,[%s] STT failed for voice attachment: %s: %s) r   r   rY   r  r  r  r   r   r  rE   ru   r   rA  r   r   tempfileNamedTemporaryFilewriter   _convert_audio_to_wav_filer   exists	_call_sttrv   unlinkOSErrorr   HTTPStatusErrorTransportErrorIOErrorr   r&   )r"   r   rK  rM  r  r  download_url
is_pre_wavr  download_headersr   
audio_datar  tmpwav_pathr  r   s                    r$   r  zQQAdapter._stt_voice_attachment  s     &  	"LL7W[X[W[H\   "! 
 	]''-- 9 8 8 8(LJLLLdm\\\000000{<(( 	NN<l3B3>OPPP4R	$ 94=IIIt#5577LLNSbS!%&&   *..(!%	 /        D !!###JLL@J  ;;	   :##NM
OO  
 t   00u0MM (QTIIj)))"xH( ( ( ( ( ( ( ( ( ( ( ( ( ( ( KM
OO    >x   "&!@!@X!V!VVVVVVV  tH~~'<'<'>'>  NNH$-    4 LL6xPPP#~~h77777777J	(####     Y3T]JtPStDTUUUUH$-XXX%u';WE 	 	 	NN>S		"	   44444	su   +'N DN *N H0$N 0H44N 7H48B2N ,<N )L> =N >
MN 
MAN !O<=4O77O<r  bytesc           	       K   ddl }t          |          j        r&t          |          j                                        n|                     |          }t
                              d| j        t          |          ||dd                    |	                    |d          5 }|
                    |           |j        }ddd           n# 1 swxY w Y   |                    dd          d         d	z   }|                     ||           d{V }|s|                     ||           d{V }|s|                     ||           d{V }	 t!          j        |           n# t$          $ r Y nw xY w|S )
a"  Convert audio bytes to a temp .wav file using pilk (SILK) or ffmpeg.

        QQ voice messages are typically SILK format which ffmpeg cannot decode.
        Strategy: always try pilk first, fall back to ffmpeg if pilk fails.

        Returns the wav file path, or None on failure.
        r   Nz7[%s] STT: audio_data size=%d, ext=%r, first_20_bytes=%r   Fr  .r   r  )r  r   r  r{   _guess_ext_from_datar   r   rY   r   r  r  r   rsplit_convert_silk_to_wav_convert_ffmpeg_to_wav_convert_raw_to_wavrv   r  r  )	r"   r  rM  r  r  tmp_srcsrc_pathr  results	            r$   r  z$QQAdapter._convert_audio_to_wav_file  s      	 H~~$7DNN!'')))**:66 	
 	EM
OOssO	
 	
 	
 ((E(BB 	$gMM*%%%|H	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ ??3**1-6 008DDDDDDDD  	K66xJJJJJJJJF  	J33JIIIIIIIIF	Ih 	 	 	D	 s$   %CCCE& &
E32E3r   c                6   | dd         dk    s| dd         dk    rdS | dd         dk    rdS | dd	         d
k    rdS | dd	         dk    rdS | dd         dv rdS | dd	         dk    s| dd	         dk    rdS | dd	         dk    s| dd	         dk    rdS dS )z&Guess file extension from magic bytes.N	   	   #!SILK_V3      #!SILKr  r     !r/  s   RIFFr  s   fLaCr  )s   s   s   r  s   0&us   OggSr  s       s      r  rI   r   s    r$   r  zQQAdapter._guess_ext_from_data  s     8|##tBQBx9'<'<78x78w68w78>>>68***d2A2h:M.M.M68***d2A2h:M.M.M6vr%   c                V    | dd         dk    p| dd         dk    p| dd         dk    S )z+Check if bytes look like a SILK audio file.Nr/  r  r  r  r  r  rI   r  s    r$   _looks_like_silkzQQAdapter._looks_like_silk  s;     BQBx9$XRaRH(<XRaRL@XXr%   r  r  c                  K   	 ddl }n1# t          $ r$ t                              d| j                   Y dS w xY w	 |                    ||d           t          |                                          rt          |                                          j	        dk    rZt          
                    d| j        t          |          j        t          |                                          j	                   |S n8# t          $ r+}t          
                    d| j        |           Y d}~nd}~ww xY w|                    d	d
          d         dz   }	 ddl}|                    ||           |                    ||d           t          |                                          rt          |                                          j	        dk    rt          
                    d| j        t          |          j        t          |                                          j	                   |	 t!          j        |           S # t$          $ r Y S w xY wn8# t          $ r+}t          
                    d| j        |           Y d}~nd}~ww xY w	 t!          j        |           n:# t$          $ r Y n.w xY w# 	 t!          j        |           w # t$          $ r Y w w xY wxY wdS )zConvert audio file to WAV using the pilk library.

        Tries the file as-is first, then as .silk if the extension differs.
        pilk can handle SILK files with various headers (or no header).
        r   NuK   [%s] pilk not installed — cannot decode SILK audio. Run: pip install pilk>  )rate,   z([%s] pilk converted %s to wav (%d bytes)z&[%s] pilk direct conversion failed: %sr  r   r  z3[%s] pilk converted %s (as .silk) to wav (%d bytes)z%[%s] pilk .silk conversion failed: %s)pilkImportErrorr   r   rY   silk_to_wavr   r  statst_sizer   r   r   r  shutilcopy2rv   r  r  )r"   r  r  r  r   	silk_pathr  s          r$   r  zQQAdapter._convert_silk_to_wav  s&     	KKKK 	 	 	NN]   44		WXxe<<<H~~$$&&  4>>+>+>+@+@+H2+M+M>MNN'NN''))1	     	W 	W 	WLLA4=RUVVVVVVVV	W OOC++A.8		MMMLL9---Yu===H~~$$&&  4>>+>+>+@+@+H2+M+MIMNN'NN''))1	    	)$$$$     	V 	V 	VLL@$-QTUUUUUUUU	V	)$$$$   	)$$$$    ts   	 *77B<C9 9
D.!D))D.CI (H==
I
	I
J, 
J!I>9J, >JJ, J 
J)(J),K.KK
KKKKc                  K   	 ddl }|                    |d          5 }|                    d           |                    d           |                    d           |                    |           ddd           n# 1 swxY w Y   |S # t          $ r,}t                              d| j	        |           Y d}~dS d}~ww xY w)u   Last resort: try writing audio data as raw PCM 16-bit mono 16kHz WAV.

        This will produce garbage if the data isn't raw PCM, but at least
        the ASR engine won't crash — it'll just return empty.
        r   Nwr   r  r  z [%s] raw PCM fallback failed: %s)
waverk   setnchannelssetsampwidthsetframeratewriteframesr   r   r   rY   )r"   r  r  r  wfr   s         r$   r  zQQAdapter._convert_raw_to_wav  s     	KKK8S)) +R""""""&&&z***	+ + + + + + + + + + + + + + +
 O 	 	 	LL;T]CPPP44444	s;   B AA?3B ?BB BB 
C!B==Cc                4  K   	 t          j        ddd|dddd|t           j        j        t           j        j                   d	{V }t          j        |                                d
           d	{V  |j        dk    rz|j        r|j        	                                 d	{V nd}t                              d| j        t          |          j        |d	d                             d                     d	S nE# t           j        t"          f$ r,}t                              d| j        |           Y d	}~d	S d	}~ww xY wt          |                                          r*t          |                                          j        dk    r5t                              d| j        t          |          j                   d	S t                              d| j        t          |          j        t          |                                          j                   |S )z'Convert audio file to WAV using ffmpeg.ffmpegz-yz-iz-ar16000z-ac1)stdoutstderrN   r   r   r%   z[%s] ffmpeg failed for %s: %s   replace)errorsz [%s] ffmpeg conversion error: %sr  z+[%s] ffmpeg produced no/small output for %sz*[%s] ffmpeg converted %s to wav (%d bytes))r   create_subprocess_exec
subprocessDEVNULLPIPEwait_forwait
returncoder  readr   r   rY   r   r   decodeTimeoutErrorFileNotFoundErrorr  r  r  r   )r"   r  r  procr  r   s         r$   r  z QQAdapter._convert_ffmpeg_to_wav&  s4     	 7)1).        D "499;;;;;;;;;;;;!##59[It{//111111111c3MNN'4C4L''y'99	   t $ $&78 	 	 	NN=t}cRRR44444	 H~~$$&& 	$x..*=*=*?*?*G2*M*MNN=X#  
 48MNNNN!!)		
 	
 	
 s   C4C; ;D=!D88D=Optional[Dict[str, str]]c                   | j         j        pi }|                    d          }t          |t                    r|                    d          dur|                    d          p|                    dd          }|                    d          p|                    dd          }|                    d	d          }|r|r|                    d
          ||pddS |rB|                    dd          }dddd}|                    |d          }|r|||p|dv rdnddS t          j        dd          }|rCt          j        dd          }t          j        dd          }|                    d
          ||dS dS )uz  Resolve STT backend configuration from config/environment.

        Priority:
        1. Plugin-specific: ``channels.qqbot.stt`` in config.yaml → ``self.config.extra["stt"]``
        2. QQ-specific env vars: ``QQ_STT_API_KEY`` / ``QQ_STT_BASE_URL`` / ``QQ_STT_MODEL``
        3. Return None if nothing is configured (STT will be skipped, QQ built-in ASR still works).
        sttenabledFbaseUrlbase_urlr   apiKeyapi_keymodel/z	whisper-1)r  r  r  providerzaiz+https://open.bigmodel.cn/api/coding/paas/v4zhttps://api.openai.com/v1)r  openaiglm)r  r!  zglm-asrQQ_STT_API_KEYQQ_STT_BASE_URLQQ_STT_MODELN)rd   rt   ru   r"  r#  rstriprv   rw   )	r"   rt   stt_cfgr  r  r  r  _PROVIDER_BASE_URLS
qq_stt_keys	            r$   _resolve_stt_configzQQAdapter._resolve_stt_configS  s    !'R ))E""gt$$ 	Y)?)?u)L)L{{9--LZ1L1LHkk(++Iw{{9b/I/IGKK,,E G  ( 4 4&"1k    ";;z599 I9H' '#
 /228R@@ $,#*!& "_2:n2L2LYYR]	   Y/44
 
	y!= H Ini88E$OOC00%   tr%   c           	       K   |                                  }|s"t                              d| j                   dS |d         }|d         }|d         }	 t	          |d          5 }| j                            | ddd	| id
t          |          j        |dfid|id           d{V }ddd           n# 1 swxY w Y   |	                                 |
                                }|                    dg           }	|	rX|	d                             di                               dd          }
|
                                r|
                                S |                    dd          }|                                r|                                S dS # t          j        t          f$ r6}t                              d| j        ||dd         |           Y d}~dS d}~ww xY w)a  Call an OpenAI-compatible STT API to transcribe a wav file.

        Uses the provider configured in ``channels.qqbot.stt`` config,
        falling back to QQ's built-in ``asr_refer_text`` if not configured.
        Returns None if STT is not configured or the call fails.
        z9[%s] STT not configured (no stt config or QQ_STT_API_KEY)Nr  r  r  rbz/audio/transcriptionsr   zBearer filez	audio/wavrq   )r   filesr   r   choicesr   r   rA  r   rZ  z0[%s] STT API call failed (model=%s, base=%s): %srS   )r)  r   r   rY   rk   r   r   r   r   r   r   ru   rx   r   r  r  )r"   r  r&  r  r  r  fr   r  r.  rA  rZ  r   s                r$   r  zQQAdapter._call_stt  sb      **,, 	NNK   4:&)$ 	h%% !.33666,.A.A.AB!DNN$7K#HI!5)  4                       !!###YY[[FjjB//G +!!*..B77;;IrJJ==?? +"==??*::fb))Dzz|| $zz||#4%w/ 	 	 	NNB"   44444	sD   F $AB7+F 7B;;F >B;?BF =F G&0+G!!G&
source_urlc                \  K   ddl }t          |          j        r8t          t          |          j                  j                                        nd}|r|dvr|                     |          }|                    |d          5 }|                    |           |j	        }ddd           n# 1 swxY w Y   |
                    dd          d         d	z   }	 |d
k    p|                     |          }|r|                     ||           d{V }	n|                     ||           d{V }	|	sbt                              d| j        |dd         |           t#          |d|           	 t%          j        |           S # t(          $ r Y S w xY wnH# t*          $ r; t#          |d|           cY 	 t%          j        |           S # t(          $ r Y S w xY ww xY w	 	 t%          j        |           n:# t(          $ r Y n.w xY w# 	 t%          j        |           w # t(          $ r Y w w xY wxY w	 t          |                                          }
t%          j        |           t#          |
d          S # t*          $ r,}t                              d| j        |           Y d}~dS d}~ww xY w)zLConvert audio bytes to .wav using pilk (SILK) or ffmpeg, caching the result.r   Nr   )r  r  r  r  r  r  r  r  Fr  r  r   r  r  z/[%s] audio conversion failed for %s (format=%s)rR   qq_voicezqq_voice.wavz%[%s] Failed to read converted wav: %s)r  r   r  r   r  r{   r  r  r  r   r  r  r  r  r   r   rY   r   rv   r  r  r   
read_bytesr   )r"   r  r0  r  r  r  r  r  is_silkr  wav_datar   s               r$   r  zQQAdapter._convert_audio_to_wav  s      	
 
##(D*%%*++288::: 	
  
	8c 	"
 	
 	
 ++J77C((E(BB 	$gMM*%%%|H	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ ??3**1-6	WnI(=(=j(I(IG O#888LLLLLLLL#::8XNNNNNNNN OEMssO	   1=M=M=MNN	(####   O  	K 	K 	K,Z9IC9I9IJJJJ	(####   	KO	(####   	(####   	H~~0022HIh,X~FFF 	 	 	LL@$-QTUUU44444	s   B00B47B4BF 0F
FFH G3H 5G


GGGH  G5 5
HHH,HH,
H)&H,(H))H,0AI5 5
J+?!J&&J+methodr  bodyr   floatc                  K   | j         st          d          |                                  d{V }d| dt                      d}	 | j                             |t
           | |||           d{V }|                                }|j        dk    r1t          d|j         d	| d
|                    d|                     |S # t          j
        $ r}	t          d| d|	           |	d}	~	ww xY w)z5Make an authenticated REST API request to QQ Bot API.u.   HTTP client not initialized — not connected?Nr   zapplication/json)r   zContent-Typer   )r   r   r   i  zQQ Bot API error [z] z: r   zQQ Bot API timeout [z]: )r   r`   r   rC   requestr,   r   status_coderu   r   TimeoutException)
r"   r6  r  r7  r   r   r   r   r   r   s
             r$   _api_requestzQQAdapter._api_request  s|        	QOPPP((********-e--.*,,
 
	O*22#T## 3        D 99;;D3&&"3)9 3 3T 3 3xx	4003 3   K% 	O 	O 	ODdDDsDDEE3N	Os   	A?C	 	C3C..C3target_type	target_id	file_type	file_datasrv_send_msg	file_namec                  K   |dk    rd| dnd| d}||d}	|r||	d<   n|r||	d<   |t           k    r|r||	d<   t          d	          D ]}
	 |                     d
||	t                     d{V c S # t          $ r]}t          |          t          fddD                       r |
dk     r!t          j        d|
dz   z             d{V  n Y d}~d}~ww xY wdS )z"Upload media and return file_info.rT  
/v2/users/z/files/v2/groups/)r@  rB  r   rA  rC     POSTr  Nc              3      K   | ]}|v V  	d S r  rI   )r  kwerr_msgs     r$   r  z*QQAdapter._upload_media.<locals>.<genexpr>G  s;         g     r%   )400401Invalidr   Timeoutr  g      ?r   )	rA   ranger=  r0   r`   r   r  r   r   )r"   r>  r?  r@  r   rA  rB  rC  r  r7  attemptr   rK  s               @r$   _upload_mediazQQAdapter._upload_media#  s      e## +****0y000 	 #( 
  
  	*DKK 	* )D''I' )D Qxx 	 	G!..D$0C /              
 
 
c((    "Q      Q;;!-w{(;<<<<<<<<<< =<<<<
	 	s   #A44
C>ACCg      .@g      ?c                x  K   t                               d| j        | j                   d}|| j        k     r^t	          j        | j                   d{V  || j        z  }| j        r#t                               d| j        |           dS || j        k     ^t                               d| j        | j                   dS )a  Wait for the WebSocket listener to reconnect.

        The listener loop (_listen_loop) auto-reconnects on disconnect, but
        there is a race window where send() is called right after a disconnect
        and before the reconnect completes.  This method polls is_connected
        for up to _RECONNECT_WAIT_SECONDS.

        Returns True if reconnected, False if still disconnected.
        u=   [%s] Not connected — waiting for reconnection (up to %.0fs)rr   Nz[%s] Reconnected after %.1fsTz$[%s] Still not connected after %.0fsF)	r   r   rY   _RECONNECT_WAIT_SECONDSr   r   _RECONNECT_POLL_INTERVALis_connectedr   )r"   waiteds     r$   _wait_for_reconnectionz QQAdapter._wait_for_reconnectionV  s       	SM4#?	A 	A 	At333- =>>>>>>>>>d33F  :DM6RRRt t333 	=t}dNjkkkur%   r;  reply_tometadatar   c                  K   ~| j         s,|                                  d{V st          ddd          S |r|                                st          d          S |                     |          }|                     || j                  }t          dd          }|D ],}|                     |||           d{V }|j        s|c S d}-|S )	zSend a text or markdown message to a QQ user or group.

        Applies format_message(), splits long messages via truncate_message(),
        and retries transient failures with exponential backoff.
        NFNot connectedTsuccessr   r   )r^  z	No chunksr^  r   )	rV  rX  r   rx   format_messagetruncate_messager7   _send_chunkr^  )	r"   r;  rA  rY  rZ  	formattedchunkslast_resultchunks	            r$   sendzQQAdapter.sendl  s        	X4466666666 X!%RVWWWW 	,gmmoo 	,d++++''00	&&y$2IJJ kBBB 	 	E $ 0 0% J JJJJJJJK& #""""HHr%   c           	       
K   d}|                      |          }t          d          D ]1}	 |dk    r|                     |||           d{V c S |dk    r|                     |||           d{V c S |dk    r|                     |||           d{V c S t          dd|           c S # t          $ r}|}t          |                                          
t          
fd	d
D                       rY d}~ n\|dk     rHdd|z  z  }t                              d| j        |dz   ||           t          j        |           d{V  Y d}~+d}~ww xY w|rt          |          ndt                              d| j                   t          fddD                        }	t          d|	          S )z5Send a single chunk with retry + exponential backoff.NrG  rT  ro  rx  FzUnknown chat type for r_  c              3      K   | ]}|v V  	d S r  rI   )r  kerrs     r$   r  z(QQAdapter._send_chunk.<locals>.<genexpr>  s;         S     r%   )invalid	forbidden	not foundzbad requestr  g      ?z$[%s] send retry %d/3 after %.1fs: %sr   zUnknown errorz[%s] Send failed: %sc              3  D   K   | ]}|                                 v V  d S r  )r{   )r  rj  	error_msgs     r$   r  z(QQAdapter._send_chunk.<locals>.<genexpr>  sC       
 
'(A"""
 
 
 
 
 
r%   )rl  rm  rn  r]  )_guess_chat_typerP  _send_c2c_text_send_group_text_send_guild_textr   r   r   r{   r  r   r   rY   r   r   r   )r"   r;  rA  rY  last_excrX  rQ  r   r   r   rk  rp  s             @@r$   rb  zQQAdapter._send_chunk  s      )-))'22	Qxx 	/ 	/G/%%!%!4!4Wgx!P!PPPPPPPPPP'))!%!6!6w!R!RRRRRRRRRR'))!%!6!6w!R!RRRRRRRRRR% %-Og-O-O       / / /#hhnn&&    !U      EEEEEQ;;1<0ENN>!   "-.........'/* &.BCMMM?	+T]IFFF 
 
 
 
,Q
 
 
 
 
 
	 %yINNNNs0   "B4"B49"B4B44
E>>EAEEopenidc                V  K   |                      |p|           |                     ||          }|r||d<   |                     dd| d|           d{V }t          |                    dt          j                    j        dd                             }t          d||	          S )
z%Send text to a C2C user via REST API.r-  rH  rE  	/messagesNr?     Tr^  r9  raw_response	r6  _build_text_bodyr=  r   ru   r1  r2  r3  r   )r"   rv  rA  rY  r7  r   r-  s          r$   rr  zQQAdapter._send_c2c_text  s       	8-v...$$Wh77 	&%DN&&v/MF/M/M/MtTTTTTTTTTXXdDJLL$4SbS$9::;;$6MMMMr%   rm  c                V  K   |                      |p|           |                     ||          }|r||d<   |                     dd| d|           d{V }t          |                    dt          j                    j        dd                             }t          d||	          S )
z"Send text to a group via REST API.r-  rH  rF  rx  Nr?  ry  Trz  r|  )r"   rm  rA  rY  r7  r   r-  s          r$   rs  zQQAdapter._send_group_text  s       	83|444$$Wh77 	&%DN&&9,9994
 
 
 
 
 
 
 
 TXXdDJLL$4SbS$9::;;$6MMMMr%   rs  c                  K   d|d| j                  i}|r||d<   |                     dd| d|           d{V }t          |                    dt	          j                    j        dd                             }t          d	||
          S )z*Send text to a guild channel via REST API.rA  Nr-  rH  z
/channels/rx  r?  ry  Trz  )r7   r=  r   ru   r1  r2  r3  r   )r"   rs  rA  rY  r7  r   r-  s          r$   rt  zQQAdapter._send_guild_text  s       !*73LT5L3L+MN 	&%DN&&v/QJ/Q/Q/QSWXXXXXXXXTXXdDJLL$4SbS$9::;;$6MMMMr%   c                    |                      |pd          }| j        rd|d| j                 it          |d}n|d| j                 t          |d}|r| j        sd|i|d<   |S )z2Build the message body for C2C/group text sending.defaultrA  N)markdownmsg_typemsg_seq)rA  r  r  r9  message_reference)r6  rz   r7   r;   r:   )r"   rA  rY  r  r7  s        r$   r}  zQQAdapter._build_text_body  s     $$X%:;;! 	&0I$2I0I(JK-"$ $DD ##<T%<#<=)" D  	E) E-98,D()r%   	image_urlcaptionc                ,  K   ~|                      ||t          d||           d{V }|j        s|                     |          s|S t                              d| j        |j                   |r| d| n|}|                     |||           d{V S )z-Send an image natively via QQ Bot API upload.r  Nz0[%s] Image send failed, falling back to text: %srR  )r;  rA  rY  )	_send_mediar>   r^  _is_urlr   r   rY   r   rg  )r"   r;  r  r  rY  rZ  r  fallbacks           r$   
send_imagezQQAdapter.send_image  s       ''Y 0'7H
 
 
 
 
 
 
 
 > 	i!8!8 	M 	>ML	
 	
 	

 18Fg,,,,,YYYw8YTTTTTTTTTr%   
image_pathc                R   K   ~|                      ||t          d||           d{V S )z!Send a local image file natively.r  N)r  r>   )r"   r;  r  r  rY  kwargss         r$   send_image_filezQQAdapter.send_image_file  O       %%Z!17GX
 
 
 
 
 
 
 
 	
r%   
audio_pathc                R   K   ~|                      ||t          d||           d{V S )zSend a voice message natively.r~  N)r  r@   )r"   r;  r  r  rY  r  s         r$   
send_voicezQQAdapter.send_voice-  r  r%   
video_pathc                R   K   ~|                      ||t          d||           d{V S )zSend a video natively.r  N)r  r?   )r"   r;  r  r  rY  r  s         r$   
send_videozQQAdapter.send_video;  r  r%   	file_pathc           	     V   K   ~|                      ||t          d|||           d{V S )zSend a file/document natively.r,  )rC  N)r  rA   )r"   r;  r  r  rC  rY  r  s          r$   send_documentzQQAdapter.send_documentI  s[       %% & 
 
 
 
 
 
 
 
 	
r%   media_sourcekindc                8  K   | j         s,|                                  d{V st          ddd          S 	 |                     ||           d{V \  }}	}
|                     |          }|dk    rt          dd          S |                     ||||                     |          s|nd|                     |          r|ndd|t          k    r|
nd	           d{V }|                    d
          }|st          dd|           S | 	                    |          }t          d
|i|d}|r|d| j                 |d<   |r||d<   |                     d|dk    rd| dnd| d|           d{V }t          dt          |                    dt          j                    j        dd                             |          S # t"          $ rI}t$                              d| j        |           t          dt          |                    cY d}~S d}~ww xY w)z*Upload media and send as a native message.NFr\  Tr]  rx  z,Guild media send not supported via this pathr_  )rA  r   rB  rC  	file_infozUpload returned no file_info: )r  mediar  rA  r-  rH  rT  rE  rx  rF  r?  ry  rz  z[%s] Media send failed: %s)rV  rX  r   _load_mediarq  rR  r  rA   ru   r6  r<   r7   r=  r   r1  r2  r3  r   r   r   rY   )r"   r;  r  r@  r  r  rY  rC  r   rK  resolved_namerX  uploadr  r  r7  	send_datar   s                     r$   r  zQQAdapter._send_media^  s        	X4466666666 X!%RVWWWW=	=6:6F6Fi7 7 1 1 1 1 1 1-D,
 --g66IG## "!)W   
  --&*ll<&@&@J$$d$(LL$>$>HLLD"+4+G+G--T .        F 

;//I !!)R&)R)R   
 ((11G*%y1"$ $D
  E")*CD,C*C"DY *!)X"// !E)) 433339w999       I y}}T4:<<3CCRC3HIIJJ&   
  	= 	= 	=LL5t}cJJJe3s88<<<<<<<<<	=s,   AG BG B:G 
H>HHHr:  Tuple[str, str, str]c                  K   t          |                                          }|st          d          t          |          }|j        dv r>t          j        |          d         pd}|pt          |j                  j	        pd}|||fS t          |          
                                }|                                s(t          j                    |z                                  }|                                r|                                sL|                    d          st#          |          dk     rt          d|          t%          d	|           |                                }|p|j	        }t          j        t          |                    d         pd}t)          j        |                              d
          }|||fS )zSLoad media from URL or local path. Returns (base64_or_url, content_type, filename).zMedia source is requiredhttphttpsr   zapplication/octet-streamr  <rG  z1Invalid media source (looks like a placeholder): zMedia file not found: ascii)r   rx   r  r   schemer  
guess_typer   r  r   
expanduseris_absolutecwdresolver  is_filer  r   r  r3  base64	b64encoder  )	r"   r:  rC  parsedrK  r  
local_pathr)  b64s	            r$   r  zQQAdapter._load_media  s      V""$$ 	97888&!!=---$/77:X>XL%Jfk):):)?J7M<66 &\\,,..
%%'' 	=(**z1::<<J  "" 	K**<*<*>*> 	K   %% Vq RRR   $$IZ$I$IJJJ##%%!4Z_$S__55a8V<V 	 s##**733L-//r%   c                *  K   | j         sdS |                     |          }|dk    rdS | j                            |          }|sdS t	          j                    }| j                            |d          }||z
  | j        k     rdS 	 |                     |          }t          |d| j	        d|d}| 
                    dd| d	|           d{V  || j        |<   dS # t          $ r,}	t                              d
| j        |	           Y d}	~	dS d}	~	ww xY w)u  Send an input notify to a C2C user (only supported for C2C).

        Debounced to one request per ~50s (the API sets a 60s indicator).
        The QQ API requires the originating message ID — retrieved from
        ``_last_msg_id`` which is populated by ``_on_message``.
        NrT  rr   r   )
input_typeinput_second)r  r-  input_notifyr  rH  rE  rx  z[%s] send_typing failed: %s)rV  rq  r   ru   r   r   _TYPING_DEBOUNCE_SECONDSr6  r=   _TYPING_INPUT_SECONDSr=  r   r   r   rY   )
r"   r;  rZ  rX  r-  now	last_sentr  r7  r   s
             r$   send_typingzQQAdapter.send_typing  sv        	F))'22	F"&&w// 	F ikk(,,Wc::	?T:::F	L((11G1 "#$($>! ! # D ##F,K,K,K,KTRRRRRRRRR,/D ))) 	L 	L 	LLL6sKKKKKKKKK	Ls   AC 
D&!DDc                2    | j         r|S t          |          S )zFormat message for QQ.

        When markdown_support is enabled, content is sent as-is (QQ renders it).
        When disabled, strip markdown via shared helper (same as BlueBubbles/SMS).
        )rz   r   )r"   rA  s     r$   r`  zQQAdapter.format_message	  s"     ! 	Ng&&&r%   c                F   K   |                      |          }||dv rdnddS )z/Return chat info based on chat type heuristics.)ro  rx  ro  rU  )r   r   )rq  )r"   r;  rX  s      r$   get_chat_infozQQAdapter.get_chat_info	  s=      ))'22	(,>>>GGD
 
 	
r%   c                H    t          t          |                     j        dv S )Nr  )r   r   r  )r:  s    r$   r  zQQAdapter._is_url	  s    F$$+/@@@r%   c                2    || j         v r| j         |         S dS )zDDetermine chat type from stored inbound metadata, fallback to 'c2c'.rT  )r   )r"   r;  s     r$   rq  zQQAdapter._guess_chat_type 	  s#    d)))&w//ur%   c                `    ddl }|                    dd|                                           }|S )z9Strip the @bot mention prefix from group message content.r   Nz^@\S+\s*r   )resubrx   )rA  r  strippeds      r$   rq  zQQAdapter._strip_at_mention&	  s/     				66+r7==??;;r%   rW  c                l    | j         dk    rdS | j         dk    r|                     | j        |          S dS NdisabledF	allowlistT)r|   _entry_matchesr}   )r"   rW  s     r$   r_  zQQAdapter._is_dm_allowed/	  s?    ?j((5?k))&&t'7AAAtr%   group_idc                l    | j         dk    rdS | j         dk    r|                     | j        |          S dS r  )r~   r  r   )r"   r  rW  s      r$   rp  zQQAdapter._is_group_allowed6	  sA    ++5,,&&t'=xHHHtr%   entriesrL   targetc                    t          |                                                                          }| D ]D}t          |                                                                          }|dk    s||k    r dS EdS )N*TF)r   rx   r{   )r  r  normalized_targetentry
normalizeds        r$   r  zQQAdapter._entry_matches=	  s    KK--//5577 	 	EU))++1133JS  J2C$C$Ctt %Dur%   r   c                j   |st          j        t          j                  S 	 t          j        |          S # t
          t          f$ r Y nw xY w	 t          j        t          |          dz  t          j                  S # t
          t          f$ r Y nw xY wt          j        t          j                  S )zParse QQ API timestamp (ISO 8601 string or integer ms).

        The QQ API changed from integer milliseconds to ISO 8601 strings.
        This handles both formats gracefully.
        )tzi  )	r   r  r   utcfromisoformatr  	TypeErrorfromtimestampr   )r"   r)  s     r$   rf  zQQAdapter._parse_qq_timestampF	  s      	1<8<0000	)#...I& 	 	 	D		)#c((T/hlKKKKI& 	 	 	D	|x|,,,,s!   7 A
A/A? ?BBc                    t          j                     }t          | j                  t          k    r4|t          z
  fd| j                                        D             | _        || j        v rdS || j        |<   dS )Nc                (    i | ]\  }}|k    ||S rI   rI   )r  keytscutoffs      r$   
<dictcomp>z+QQAdapter._is_duplicate.<locals>.<dictcomp>\	  s+     # # ##Cb6kkRkkkr%   TF)r   r   r   r9   r8   items)r"   r-  r  r  s      @r$   rC  zQQAdapter._is_duplicateX	  s    ikkt"##n44//F# # # #'+':'@'@'B'B# # #D T(((4&)F#ur%   )rD   r   )r   r   rD   rZ   )rd   r   rD   rE   )rD   rZ   )r   r   rD   rZ   )r   r   rD   rE   )r   r  rD   rZ   )r   r   rD   rZ   )r)  r   rD   r*  )r-  r   rD   r   )r7  r   rD   rZ   )r=  r   r   r   rD   rZ   )r   r  r-  r   rA  r   rB  r  r@  r   rD   rZ   )r]  r`  r^  r`  )rJ  r   rD   r  )r   r   rK  r   rD   r  )rK  r   rM  r   rD   rE   )rD   r  )r   r   rK  r   rM  r   r  r  r  r  rD   r  )r  r  rM  r   rD   r  )r   r  rD   r   )r   r  rD   rE   )r  r   r  r   rD   r  )r  r  r  r   rD   r  )rD   r  )r  r   rD   r  )r  r  r0  r   rD   r  )
r6  r   r  r   r7  r*  r   r8  rD   r  )NNFN)r>  r   r?  r   r@  r   r   r  rA  r  rB  rE   rC  r  rD   r  )NN)
r;  r   rA  r   rY  r  rZ  r*  rD   r   r  )r;  r   rA  r   rY  r  rD   r   )rv  r   rA  r   rY  r  rD   r   )rm  r   rA  r   rY  r  rD   r   )rs  r   rA  r   rY  r  rD   r   )rA  r   rY  r  rD   r  )NNN)r;  r   r  r   r  r  rY  r  rZ  r*  rD   r   )
r;  r   r  r   r  r  rY  r  rD   r   )
r;  r   r  r   r  r  rY  r  rD   r   )
r;  r   r  r   r  r  rY  r  rD   r   )r;  r   r  r   r  r  rC  r  rY  r  rD   r   )r;  r   r  r   r@  r   r  r   r  r  rY  r  rC  r  rD   r   )r:  r   rC  r  rD   r  )r;  r   rD   rZ   )rA  r   rD   r   )r;  r   rD   r  )r:  r   rD   rE   )r;  r   rD   r   )rW  r   rD   rE   )r  r   rW  r   rD   rE   )r  rL   r  r   rD   rE   )r)  r   rD   r   )r-  r   rD   rE   )Sr&   r'   r(   r)   SUPPORTS_MESSAGE_EDITINGr7   r  r  propertyrY   rc   r!   r   r   r   r   r   r   r   r   r   r   r   r  r  staticmethodr  r   r$  r   r6  r<  r%  rD  rE  rF  rG  re  rb  r  r  r  r  r  r  r  r  r  r  r)  r  r  r/   r=  rR  rT  rU  rX  rg  rb  rr  rs  rt  r}  r  r  r  r  r  r  r  r  r`  r  r  rq  rq  r_  rp  r  rf  rC  r*   r+   s   @r$   rQ   rQ      s       TT  %+!   X( ( ( (.; .; .; .; .; .;h    X7 7 7 7r8 8 8 82( ( ( (0!& !& !& !&F   4R R R R(W% W% W% W%r   .7 7 7 7&   &$Q $Q $Q $QL" " " "B 
 
 \
3? 3? 3? 3?jV V V V > > > \> * * * \*, , , , , ,Q Q Q Q6V) V) V) V)p8) 8) 8) 8)tB) B) B) B)H=) =) =) =)F       \ &k
 k
 k
 k
Z!= != != !=F    \*	 	 	 	" -1+/z z z z z zx0 0 0 0d    \& Y Y Y \Y4 4 4 4l   &+ + + +Z6 6 6 6p1 1 1 1f; ; ; ;J .20"O "O "O "O "OR "&'+!&'+, , , , ,^ #"   4 '+15    J '+	0O 0O 0O 0O 0Of HLN N N N N NRN N N N N  LP
N 
N 
N 
N 
N ;?    D &*&*15U U U U U< &*&*
 
 
 
 
$ &*&*
 
 
 
 
$ &*&*
 
 
 
 
$ &*'+&*
 
 
 
 
6 &*&*'+L= L= L= L= L=^ ;?$0 $0 $0 $0 $0T&L &L &L &L &LX' ' ' '
 
 
 
 A A A \A       \          \- - - -$
 
 
 
 
 
 
 
r%   rQ   r  )rK   r   rD   rL   )Lr)   
__future__r   r   r  r   loggingr  rv   r   r1  r   r   pathlibr   typingr   r   r	   r
   r   urllib.parser   r   rG   r  r   rH   gateway.configr   r   gateway.platforms.baser   r   r   r   r   r   r   gateway.platforms.helpersr   	getLoggerr&   r   r   r   !gateway.platforms.qqbot.constantsr,   r-   r.   r/   r0   r1   r2   r3   r4   r5   r6   r7   r8   r9   r:   r;   r<   r=   r>   r?   r@   rA   gateway.platforms.qqbot.utilsrB   rN   rC   rJ   rO   rQ   rI   r%   r$   <module>r     s   > # " " " " "         				   ' ' ' ' ' ' ' '       3 3 3 3 3 3 3 3 3 3 3 3 3 3 ! ! ! ! ! !NNN   GGGLLLOO   OEEE 4 3 3 3 3 3 3 3                  5 4 4 4 4 4		8	$	$	W 	W 	W 	W 	W9 	W 	W 	W                                                0       1 1 1 1
$ $ $ $W# W# W# W# W## W# W# W# W# W#s$   A 	AA#A* *	A65A6