
    `gjN                   D   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mZmZ ddlmZ 	 ddlZdZ	 ddlZdZdd
lmZm Z  ddl!m"Z"m#Z#m$Z$m%Z%m&Z&m'Z'm(Z( ddl)m*Z*  ejV                  e,      Z- G d de.      Z/ddl0m1Z1m2Z2m3Z3m4Z4m5Z5m6Z6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z<m=Z=m>Z>m?Z?m@Z@mAZAmBZBmCZCmDZDmEZEmFZF ddlGmHZImJZJ ddlKmLZLmMZMmNZN ddlOmPZPmQZQmRZRmSZSmTZTmUZUmVZVmWZW ddZXddZY G d de"      ZZy# e$ r d	ZdZY w xY w# e$ r d	ZdZY w xY w)um  
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: "pairing"             # open | allowlist | disabled | pairing
          allow_from: ["openid_1"]
          group_policy: "pairing"          # open | allowlist | disabled | pairing
          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	AwaitableCallable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       y )N zWebSocket closed (code=z	, reason=))intcodestrreasonsuper__init__)selfr   r!   	__class__s      R/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/qqbot/adapter.pyr#   zQQCloseError.__init__T   sG    !%CI4	%+c&k2499+Yt{{mSTUV    r   )__name__
__module____qualname____doc__r#   __classcell__r%   s   @r&   r   r   N   s    
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)ChunkedUploaderUploadDailyLimitExceededErrorUploadFileTooLargeError)ApprovalRequestInlineKeyboardInteractionEventbuild_approval_keyboardbuild_update_prompt_keyboardparse_approval_button_dataparse_interaction_eventparse_update_prompt_button_datac                     t         xr t        S )z/Check if QQ runtime dependencies are available.)AIOHTTP_AVAILABLEHTTPX_AVAILABLE r'   r&   check_qq_requirementsrV      s    00r'   c                    t        |       S )z0Coerce config values into a trimmed string list.)_coerce_list_impl)values    r&   _coerce_listrZ      s    U##r'   c                      e Zd ZdZdZeZdZdZedgd       Z	dhdZ
didZedjd       Zdk fd	Zedgd
       Zedjd       ZdddldZdidZdidZdgdZdgdZdmdZdidZdndZdidZdidZdidZdidZed        ZdodZdpdZedqd       Z edrd       Z!ds fdZ"dtdZ#	 	 	 	 dud Z$dpd!Z%	 dv	 	 	 	 	 dwd"Z&d#d$d%d&Z'edxd'       Z(	 	 	 	 	 	 dyd(Z)	 	 	 	 dzd)Z*ed{d|d*       Z+	 	 	 	 	 	 	 	 	 	 	 	 d}d+Z,	 	 	 	 	 	 	 	 	 	 	 	 d}d,Z-	 	 	 	 	 	 	 	 	 	 	 	 d}d-Z.	 	 	 	 	 	 	 	 	 	 	 	 d}d.Z/	 	 	 	 d~d/Z0edd0       Z1edd1       Z2	 	 	 	 dd2Z3	 d{	 	 	 	 	 	 	 dd3Z4edd4       Z5dd5Z6d6d6d7	 	 	 	 	 	 	 	 	 	 	 dd8Z7	 	 	 	 	 	 dd9Z8edd:       Z9edd;       Z:dd<Z;dd=Z<dd>Z=dd?Z>dd@Z?	 	 	 	 	 	 ddAZ@d6eAf	 	 	 	 	 	 	 	 	 ddBZB	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddCZCdDZDdEZEdjdFZF	 	 d	 	 	 	 	 	 	 	 	 ddGZG	 d	 	 	 	 	 	 	 ddHZH	 	 d	 	 	 	 	 	 	 	 	 ddIZI	 	 d	 	 	 	 	 	 	 	 	 ddJZJ	 d	 	 	 	 	 	 	 ddKZK	 d	 	 	 	 	 	 	 	 	 ddLZL	 d	 	 	 	 	 	 	 ddMZM	 	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddNZNdOZO	 	 	 d	 	 	 	 	 	 	 	 	 	 	 ddPZP	 d	 	 	 	 	 ddQZQ	 	 	 d	 	 	 	 	 	 	 	 	 	 	 ddRZR	 	 d	 	 	 	 	 	 	 	 	 ddSZS	 	 d	 	 	 	 	 	 	 	 	 ddTZT	 	 d	 	 	 	 	 	 	 	 	 ddUZU	 	 	 d	 	 	 	 	 	 	 	 	 	 	 ddVZV	 	 	 d	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddWZW	 	 	 	 	 	 	 	 	 	 	 	 ddXZX	 d	 	 	 	 	 ddYZYdddZZZdd[Z[dd\Z\edd]       Z]dd^Z^edd_       Z_djd`Z`ddaZaddbZbddcZceddd       ZdddeZeddfZf xZgS )	QQAdapterzJQQ Bot adapter backed by the official QQ Bot WebSocket Gateway + REST API.F<   2   c                ,    t        | dd      }|rd| S y)z>Log prefix including app_id for multi-instance disambiguation._app_idNzQQBot:QQBot)getattr)r$   app_ids     r&   _log_tagzQQAdapter._log_tag   s%     y$/F8$$r'   c                    | j                   j                         D ]-  }|j                         r|j                  t	        |             / | j                   j                          y)z"Fail all pending response futures.N)_pending_responsesvaluesdoneset_exceptionRuntimeErrorclear)r$   r!   futs      r&   _fail_pendingzQQAdapter._fail_pending   sP    **113 	8C88:!!,v"67	8 	%%'r'   c                H    | j                   ry| j                  dddd       y)aU  Mark QQ WS down without stopping the reconnect loop.

        BasePlatformAdapter uses _running for both process lifecycle and
        connection status. QQBot needs to keep the listener task alive across
        transient transport drops so it can continue reconnect attempts after a
        short-lived gateway or network failure.
        Ndisconnected)platform_state
error_codeerror_message)has_fatal_error_write_runtime_status_safer$   s    r&   _mark_transport_disconnectedz&QQAdapter._mark_transport_disconnected   s/     '')	 	( 	
r'   c                z    t        | j                  xr% | j                  xr | j                  j                         S )z;Return True only when the QQ WebSocket transport is usable.)bool_running_wsclosedru   s    r&   is_connectedzQQAdapter.is_connected   s,     DMMFdhhFtxx3FGGr'   c                   t         |   |t        j                         |j                  xs i }t        |j                  d      xs t        j                  dd            j                         | _
        t        |j                  d      xs t        j                  dd            j                         | _        t        |j                  dd            | _        t        |j                  dd	            j                         j                         | _        t!        |j                  d
      xs |j                  d            | _        t        |j                  dd	            j                         j                         | _        t!        |j                  d      xs |j                  d            | _        d | _        d | _        d | _        d | _        d | _        d| _        d | _        d | _        i | _        i | _        i | _        i | _        i | _         d | _!        d| _"        tG        jH                         | _%        i | _&        d | _'        | jP                  | _'        y )Nrc   	QQ_APP_IDr   client_secretQQ_CLIENT_SECRETmarkdown_supportT	dm_policypairing
allow_from	allowFromgroup_policygroup_allow_fromgroupAllowFrom      >@        ))r"   r#   r   QQBOTextrar    getosgetenvstripr`   _client_secretrx   _markdown_supportlower
_dm_policyrZ   _allow_from_group_policy_group_allow_from_sessionrz   _http_client_listen_task_heartbeat_task_heartbeat_interval_session_id	_last_seq_chat_type_maprf   _seen_messages_last_msg_id_typing_sent_at_access_token_token_expires_atasyncioLock_token_lock_upload_cache_interaction_callback_default_interaction_dispatch)r$   configr   r%   s      r&   r#   zQQAdapter.__init__   s   0"599X.L"))K2LMSSU!IIo&K"))4F*K

%' 	 "&eii0BD&I!J eiiY?@FFHNNP'IIl#=uyy'=
 !>9!EFLLNTTV!-II()HUYY7G-H"

 :>>B9=487;*. *.(,.0 >@02 -/13 -1(+"<<> 9;  	" &*%G%G"r'   c                     y)Nra   rU   ru   s    r&   namezQQAdapter.name  s    r'   c                     y)zAQQBot gates DM/group access at intake via dm_policy/group_policy.TrU   ru   s    r&   enforces_own_access_policyz$QQAdapter.enforces_own_access_policy  s     r'   )is_reconnectc                 K   t         s8d}| j                  d|d       t        j                  d| j                  |       yt
        s8d}| j                  d|d       t        j                  d| j                  |       y| j                  r| j                  s8d	}| j                  d
|d       t        j                  d| j                  |       y| j                  d| j                  d      sy	 ddl	m
} t        j                  dddt        gi |             | _        | j                          d{    | j!                          d{   }t        j#                  d| j                  |       | j%                  |       d{    t'        j(                  | j+                               | _        t'        j(                  | j/                               | _        | j3                          t        j#                  d| j                         y7 7 7 # t4        $ ro}d| }| j                  d|d       t        j7                  d| j                  |d       | j9                          d{  7   | j;                          Y d}~yd}~ww xY ww)aq  
        Authenticate, obtain gateway URL, and open the WebSocket.

        Args:
            is_reconnect: False on a cold first boot; True when the
                reconnect watcher is re-establishing this platform after
                an outage. QQBot has no server-side update queue so this
                flag is accepted for interface conformance only.
        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_limitsr   response)timeoutfollow_redirectsevent_hookslimitsNz[%s] Gateway URL: %sz[%s] ConnectedzQQ startup failed: qq_connect_errorexc_info)rS   _set_fatal_errorloggerwarningrd   rT   r`   r   _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$   r   messager   gateway_urlexcs         r&   connectzQQAdapter.connect  s,     !@G!!"97d!SNN>wW>G!!"97d!SNN<dmmWU||4#6#6VG!!":Gt!TNN9dmmW= **=$,,W!	 T % 1 1!%'*>)?@,.	!D $$&&& !% 5 5 77KKK.{K --,,, !( 3 3D4E4E4G HD#*#6#6t7K7K7M#ND   "KK($--8 ' 8 -  	+C51G!!"4g!NLLDMM7TLJ--/!!'')	su   C+J
.AH 1H	2H 	H
9H HBH J
	H H H 	JAJ'I*(J=J
JJ
c                B  K   d| _         | j                          | j                  r6| j                  j                          	 | j                   d{    d| _        | j                  r6| j                  j                          	 | j                   d{    d| _        | j                          d{    | j                          t        j                  d| j                         y7 # t        j
                  $ r Y w xY w7 o# t        j
                  $ r Y w xY w7 kw)z)Close all connections and stop listeners.FNz[%s] Disconnected)ry   _mark_disconnectedr   cancelr   CancelledErrorr   r   r   r   r   rd   ru   s    r&   
disconnectzQQAdapter.disconnectZ  s     !$$&'''' !%D  '')**** $(D mmo##%'7 ())  +))  	s}   >DC) C'C) -DD DD D1D25D'C) )C?<D>C??DD DDDDc                v  K   | j                   r8| j                   j                  s"| j                   j                          d{    d| _         | j                  r8| j                  j                  s"| j                  j                          d{    d| _        | j                  r)| j                  j                          d{    d| _        | j                  j                         D ]-  }|j                         r|j                  t        d             / | j                  j                          y7 7 7 tw)z*Close WebSocket, HTTP session, and client.NDisconnected)rz   r{   closer   r   acloserf   rg   rh   ri   rj   rk   )r$   rl   s     r&   r   zQQAdapter._cleanups  s     88DHHOO((.."""==!5!5--%%'''##**,,, $D **113 	@C88:!!,~">?	@ 	%%' # ( -s=   A D9D3A
D9D54D9D78D9<8D95D97D9c                  K   | j                   r0t        j                         | j                  dz
  k  r| j                   S | j                  4 d{    | j                   rAt        j                         | j                  dz
  k  r| j                   cddd      d{    S 	 | j                  j                  t        | j                  | j                  dt               d{   }|j                          |j                         }|j                  d      }|st        d|       t        |j                  dd	            }|| _         t        j                         |z   | _        t         j#                  d
| j$                  |       | j                   cddd      d{    S 7 U7 7 # t        $ r}t        d|       |d}~ww xY w7 -# 1 d{  7  sw Y   yxY ww)zFReturn a valid access token, refreshing if needed (with singleflight).r]   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   postr0   r`   r   r2   raise_for_statusr   r   rj   r   r   r   r   rd   )r$   respdatar   tokenr   s         r&   r   zQQAdapter._ensure_token  s    $))+0F0F0K"K%%%## 	& 	&!!diikD4J4JR4O&O))	& 	& 	&
	Y!..33#'<<ATATU/ 4  
 %%'yy{ HH^,E"B4&I  TXXlD9:J!&D%)YY[:%=D"KK=t}}j %%9	& 	& 	& 	&  Y"%H#NOUXXY	& 	& 	& 	&s   AG#F$G#<GG#F'G#"A F,"F*#$F,BGG#GG#'G#*F,,	G	5GG		GG#G GG G#c                  K   | j                          d{   }	 | j                  j                  t         t         d| t               dt               d{   }|j                          |j                         }|j                  d      }|st        d|       |S 7 7 I# t        $ r}t        d|       |d}~ww xY ww)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   r   r/   r1   rF   r2   r   r   r   rj   )r$   r   r   r   r   r   s         r&   r   zQQAdapter._get_gateway_url  s     ((**	T**..*-./'-eW%5"2"4 , /  D !!#99;D hhuo!FtfMNN
% +  	T!CC5IJPSS	TsD   CB#C?B' B%$B'  $C%B' '	C0B??CCc                D  K   | j                   r8| j                   j                  s"| j                   j                          d{    d| _         | j                  r8| j                  j                  s"| j                  j                          d{    d| _        t	        j
                  d      | _        t        j                  d      xsq t        j                  d      xsZ t        j                  d      xsC t        j                  d      xs, t        j                  d      xs t        j                  d	      }| j                  j                  |d
t               it        |       d{   | _         t        j                  d| j                  |       y7 V7 7 1w)z2Open a WebSocket connection to the QQ Bot gateway.NT)	trust_env	WSS_PROXY	wss_proxyHTTPS_PROXYhttps_proxy	ALL_PROXY	all_proxyr   )r   r   proxyz[%s] WebSocket connected to %s)rz   r{   r   r   aiohttpClientSessionr   r   
ws_connectrF   r4   r   r   rd   )r$   r   ws_proxys      r&   r   zQQAdapter._open_ws  sG     88DHHOO((.."""==!5!5--%%'''  --=IIk" &yy%&yy'& yy'& yy%	&
 yy% 	 11.0 , 2 
 
 	4dmm[Q3 # (
s8   A F FA
F FCF ,F-,F F F c                  K   d}d}d}| j                   r?	 t        j                         }| j                          d{    d}d}| j                   r>yy7 # t        j
                  $ r Y yt        $ r}| j                   sY d}~y|j                  }t        j                  d| j                  ||j                         t        j                         |z
  }|t        k  rn|dkD  ri|dz  }t        j                  d| j                  ||       |t        k\  r;t        j                  d| j                         | j!                  dd	d
       Y d}~yd}| j#                          | j%                  d       |dv rbdddddddddd	}|j'                  |d| d      }t        j                  d| j                  |       | j!                  d| d| d       Y d}~y|dk(  rt        j                  d| j                  t(               |t*        k\  r| j-                          Y d}~yt	        j.                  t(               d{  7   | j1                  |       d{  7  rd}d}n|dz  }Y d}~`|d k(  r.t        j                  d!| j                         d| _        d| _        |d"v r/t        j                  d#| j                  |       d| _        d| _        | j1                  |       d{  7  rd}d}nC|dz  }|t*        k\  r5t        j                  d$| j                         | j-                          Y d}~yY d}~d}~wt:        $ r}| j                   sY d}~yt        j                  d%| j                  |       | j#                          | j%                  d&       |t*        k\  r5t        j                  d'| j                         | j-                          Y d}~y| j1                  |       d{  7  rd}d}n|dz  }Y d}~d}~ww xY ww)(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   r   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  3                zinvalid opcodezinvalid payloadzinvalid shardzsharding requiredzinvalid API versionzinvalid intentzintent not authorizedzoffline/sandbox-onlybanned)	r  r  r  r  r  r  r	  r  r  zfatal error (code=r   z'[%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>   $  %  &  '  (  )  *  +  ,  -  .  /  0  1      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)ry   r   	monotonic_read_eventsr   r   r   r   r   r   rd   r!   r8   r   r9   r   r   rv   rm   r   r7   r6   r   sleep
_reconnectr   r   r   r   r   )	r$   backoff_idxconnect_timequick_disconnect_countr   r   durationfatal_descriptionsdescs	            r&   r   zQQAdapter._listen_loop  s     !"mma%#~~/''))))*& mm * ))  H}}xx>MMJJ	  >>+l:88\A=M*a/*KKB .	 .1KKd MM
 --1R&* . 
 -.*113""#67  
 
 //-13.54&
*& .11$:LTFRS8TUDLLA4==RV ))dVv&6% *   4<KK?(
 #&<<//1!--(8999!__[999&'12.#q( 4<KKO *.D&-0D*
   $ KKS
 (,D$%)DN555"#K-.*1$K"&<<%Y[_[h[hi//1 %}}94==#N113""#;<"88LL!FV++-555"#K-.*1$K!%s   P'A AA PPA P*P,	P5L7PCL7PBL7PAL7PL79H<:L7IL7#P)A:L7#K&$AL7,P7PO>PA;O>PO>(O+)O>8P>PPc                V  K   t         t        |t        t               dz
           }t        j	                  d| j
                  ||dz          t        j                  |       d{    d| _        	 | j                          d{    | j                          d{   }| j                  |       d{    | j                          t        j	                  d| j
                         y7 7 h7 R7 ;# t        $ r+}t        j                  d| j
                  |       Y d}~yd}~ww xY ww)	z<Attempt to reconnect the WebSocket. Returns True on success.r   z([%s] Reconnecting in %ds (attempt %d)...Nr   z[%s] ReconnectedTz[%s] Reconnect failed: %sF)r5   minlenr   r   rd   r   r  r   r   r   r   r   r   r   )r$   r   delayr   r   s        r&   r  zQQAdapter._reconnect  s     !#k37H3IA3M"NO6MM!O		
 mmE"""#' 		$$&&& $ 5 5 77K--,,,  "KK*DMM: 	# '7,  	NN6sK	sr   A!D)#C*$D)0C2 C,C2 C.C2 4C054C2 )D),C2 .C2 0C2 2	D&;!D!D)!D&&D)c                  K   | j                   st        d      | j                   j                  rt        d      | j                  r| j                   r}| j                   j                  se| j                   j	                          d{   }|j
                  t        j                  j                  k(  r/| j                  |j                        }|r| j                  |       n|j
                  t        j                  j                  hv rn|j
                  t        j                  j                  k(  r t        |j                  |j                        |j
                  t        j                  j                   t        j                  j"                  hv rt        d      | j                  r&| j                   r| j                   j                  sbyyyyyy7 Kw)z.Read WebSocket frames until connection closes.zWebSocket not connectedzWebSocket closedN)rz   rj   r{   ry   receivetyper   	WSMsgTypeTEXT_parse_jsonr   _dispatch_payloadPINGCLOSEr   r   CLOSEDERROR)r$   msgpayloads      r&   r  zQQAdapter._read_events  sA    xx89988??
 122mm((**Cxx7,,111**3884**73g//4466W..444"388SYY77g//668I8I8O8OPP"#566 mmmm*s   BG	G
EGGc                  K   	 | j                   rt        j                  | j                         d{    | j                  r| j                  j
                  rV	 | j                  j                  d| j                  d       d{    | j                   ryy7 g7 # t        $ r+}t        j                  d| j                  |       Y d}~Bd}~ww xY w# t        j                  $ r Y yw xY w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)ry   r   r  r   rz   r{   	send_jsonr   r   r   debugrd   r   )r$   r   s     r&   r   zQQAdapter._heartbeat_loop  s     	--mmD$<$<===xx488??R((,,ADNN-KLLL --=
 M  RLL!<dmmSQQR%% 		st   C..C B'C +B BB C C.C B 	C'!CC CC C+(C.*C++C.c                  K   | j                          d{   }dd| dddgdddd	d
d}	 | j                  rZ| j                  j                  sD| j                  j                  |       d{    t        j                  d| j                         yt        j                  d| j                         y7 7 H# t        $ r+}t        j                  d| j                  |       Y d}~yd}~ww xY w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  Fr   r   macOSzhermes-agent)z$osz$browserz$device)r   intentsshard
propertiesr8  z[%s] Identify sentz2[%s] Cannot send Identify: WebSocket not connectedz [%s] Failed to send Identify: %s)
r   rz   r{   r;  r   r   rd   r   r   r   )r$   r   identify_payloadr   s       r&   _send_identifyzQQAdapter._send_identify  s      ((**!%)' Q" .-
 		Qxxhh(()9:::0$--@H$--- +& ;  	QLL;T]]CPP	QsW   C1B6C1A B: /B80$B: C1 B: 5C18B: :	C.!C)$C1)C..C1c                H  K   | j                          d{   }dd| | j                  | j                  dd}	 | j                  rp| j                  j                  sZ| j                  j                  |       d{    t        j                  d| j                  | j                  | j                         yt        j                  d| j                         y7 7 ^# t        $ r9}t        j                  d| j                  |       d| _        d| _        Y d}~yd}~ww xY w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seqr8  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   rz   r{   r;  r   r   rd   r   r   r   )r$   r   resume_payloadr   s       r&   _send_resumezQQAdapter._send_resume  s     
 ((**!%)"..~~
	"xxhh((888>MM$$NN	 F' + 9  	"LL94==#N#D!DNN		"sW   D"C$D"A C <C=:C 7D"8 C D"C 	D&/DD"DD"c                l    	 t        j                         }|j                  |       S # t        $ r Y y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   rj   )coroloops     r&   _create_taskzQQAdapter._create_task"  s8    	++-D##D)) 		s   $' 	33c                f   |j                  d      }|j                  d      }|j                  d      }|j                  d      }t        |t              r"| j                  || j                  kD  r|| _        |dk(  rt        |t              r|ni }|j                  dd      }|d	z  d
z  | _        t        j                  d| j                  || j
                         | j                  r,| j                   | j                  | j                                y| j                  | j                                y|dk(  r|r|dk(  r| j                  |       y|dk(  r!t        j                  d| j                         y|dv r&t        j                   | j#                  ||             y|dk(  r!| j                  | j%                  |             yt        j                  d| j                  |       y|dk(  ry|dk(  rlt        j                  d| j                         | j&                  r?| j&                  j(                  s)| j                  | j&                  j+                                y|dk(  r|t-        |      nd}|s/t        j                  d| j                         d| _	        d| _        n t        j                  d| j                         | j&                  r?| j&                  j(                  s)| j                  | j&                  j+                                yt        j                  d| j                  |       y)zPRoute inbound WebSocket payloads (dispatch synchronously, spawn async handlers).r9  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GUILD_MESSAGE_CREATEDIRECT_MESSAGE_CREATEGROUP_AT_MESSAGE_CREATEGUILD_AT_MESSAGE_CREATEINTERACTION_CREATEz[%s] Unhandled dispatch: %s      z&[%s] Server requested reconnect (op 7)	   Fz<[%s] Invalid session (op 9, not resumable), clearing sessionz&[%s] Invalid session (op 9, resumable)z[%s] Unknown op: %s)r   
isinstancer   r   dictr   r   r<  rd   r   rO  rJ  rD  _handle_readyr   r   r   _on_message_on_interactionrz   r{   r   rx   )	r$   r6  r9  rQ  rR  r:  d_datainterval_ms	resumables	            r&   r0  zQQAdapter._dispatch_payload/  s   [[KKKKKKa4>>#9Q=ODN 8$Q-Q2F **%95AK'2V';c'AD$LLT((	 DNN$>!!$"3"3"56  !!$"5"5"78 7qG|""1%  i2DMMB     ##D$4$4Q$:;
 	 **!!$"6"6q"9:  :DMM1M 8
 7KK@$--Pxx!!$((.."23 7#$=QeIRMM $( !%DdmmTxx!!$((.."23*DMM2>r'   c                    t        |t              rB|j                  d      | _        t        j                  d| j                  | j                         yy)u7   Handle the READY event — store session_id for resume.rG  z[%s] Ready, session_id=%sN)r`  ra  r   r   r   r   rd   )r$   r:  s     r&   rb  zQQAdapter._handle_ready  s=    a uu\2DKK3T]]DDTDTU r'   c                    	 t        j                  |       }t        |t              r|S d S # t        $ r t        j	                  d|        Y y w xY w)Nz [QQBot] Failed to parse JSON: %r)r   loadsr   r   r   r`  ra  )rawr6  s     r&   r/  zQQAdapter._parse_json  sM    	jjoG %Wd3w==  	NN=sC	s   + AAc                    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)msg_id	time_partrands      r&   _next_msg_seqzQQAdapter._next_msg_seq  sH     		$y0	4::<##BQ',D E))r'   c                   K   |j                   rC|j                  j                  r-|j                   | j                  |j                  j                  <   t        |   |       d{    y7 w)z:Cache the last message ID per chat, then delegate to base.N)
message_idsourcechat_idr   r"   handle_message)r$   eventr%   s     r&   rz  zQQAdapter.handle_message  sO      4 46;6F6FDell223g$U+++s   A"A-%A+&A-c                  K   t        |t              syt        |j                  dd            }|r| j	                  |      r"t
        j                  d| j                  |       yt        |j                  dd            }t        |j                  dd            j                         }t        |j                  d      t              r|j                  d      ni }|dk(  r| j                  |||||       d{    y|d	v r| j                  |||||       d{    y|d
v r| j                  |||||       d{    y|dk(  r| j                  |||||       d{    yy7 m7 M7 -7 w)z(Process an inbound QQ Bot message event.Nidr   z([%s] Duplicate or missing message id: %s	timestampcontentauthorrW  >   rZ  >   rX  r[  rY  )r`  ra  r    r   _is_duplicater   r<  rd   r   _handle_c2c_message_handle_group_message_handle_guild_message_handle_dm_message)r$   
event_typer:  rr  r~  r  r  s          r&   rc  zQQAdapter._on_message  sU    !T" QUU4_%++F3LL:DMM6 k2./	aeeIr*+113$.quuX$Ex2 --**1fgvyQQQ77,,QSSSNN,,QSSS22))!VWfiPPP 3 RSSPsH   C5E-7E%8!E-E'!E-;E)<"E-E+E-'E-)E-+E-c                    || _         y)a\  Register (or clear) the interaction callback.

        Invoked once per ``INTERACTION_CREATE`` event *after* the adapter has
        ACKed the interaction. The callback is responsible for routing the
        button click to the right subsystem (approval resolver, update-prompt
        resolver, etc.) based on the ``button_data`` payload.
        N)r   )r$   callbacks     r&   set_interaction_callbackz"QQAdapter.set_interaction_callback  s     &."r'   c                n  K   t        |t              sy	 t        |      }|j                  s!t        j                  d| j                         y	 | j                  |j                         d{    t        j                  d| j                  |j                  |j                  |j                         | j                  }|,t        j                  d| j                  |j                         y	  ||       d{    y# t        $ r+}t        j                  d| j                  |       Y d}~yd}~ww xY w7 # t        $ r7}t        j                  d| j                  |j                  |       Y d}~d}~ww xY w7 # t        $ r-}t        j                  d| j                  |d	       Y d}~yd}~ww xY ww)
aZ  Handle an ``INTERACTION_CREATE`` event.

        Responsibilities:

        1. Parse the raw payload into an :class:`InteractionEvent`.
        2. ACK the interaction (``PUT /interactions/{id}``) so the client
           stops showing a loading indicator on the button.
        3. Dispatch to the registered interaction callback, if any.
        Nz+[%s] Failed to parse INTERACTION_CREATE: %sz0[%s] INTERACTION_CREATE missing id, skipping ACKz%[%s] Failed to ACK interaction %s: %sz5[%s] Interaction: scene=%s button_data=%r operator=%szA[%s] No interaction callback registered; dropping button click %rz$[%s] Interaction callback raised: %sTr   )r`  ra  rP   r   r   r   rd   r}  _acknowledge_interactionr   scenebutton_dataoperator_openidr   r<  r   )r$   r:  r{  r   r  s        r&   rd  zQQAdapter._on_interaction  s{     !T"	+A.E xxNNBDMM 	//999 	CMM5;;(9(95;P;P	

 --LLu00
 	5/!!I  	NN=t}}c 		 : 	NN7uxx 	( " 	LL6sT   	s   F5C> -F5D7 ,D5-D7 1A;F5-E< 8E:9E< =F5>	D2!D-(F5-D22F55D7 7	E7 ,E2,F52E77F5:E< <	F2#F-(F5-F22F5c                n  K   | j                   st        d      | j                          d{   }d| dt               d}| j                   j	                  t
         d| |d|it               d{   }|j                  d	k\  r(t        d
|j                   d|j                  dd        y7 7 >w)zACK a button interaction via ``PUT /interactions/{id}``.

        :param interaction_id: The ``id`` field from the
            ``INTERACTION_CREATE`` event.
        :param code: Response code (``0`` = success).
        .   HTTP client not initialized — not connected?Nr   application/jsonr   zContent-Typer   z/interactions/r   r   r   r     zInteraction ACK failed []:    )	r   rj   r   rF   putr/   r2   status_codetext)r$   interaction_idr   r   r   r   s         r&   r  z"QQAdapter._acknowledge_interaction	  s        OPP((**%eW-.*,

 &&**j~&67$'	 + 
 
 s"*4+;+;*<C99Tc?#%  # +
s"   +B5B1AB54B35=B53B5oncealwaysdeny)z
allow-oncezallow-alwaysr  c                    t        | xs d      j                  d      }t        |      dk  s|d   dk7  s|d   dk7  ry|d	   |d
   |d   d}t        |      dkD  r|d   |d<   |S )zBParse ``agent:main:<platform>:<chat_type>:<chat_id>[:<user_id>]``.r   :   r   agentr   mainNr>     rm  )platform	chat_typery  user_id)r    splitr(  )session_keypartsparseds      r&   _parse_gateway_session_keyz$QQAdapter._parse_gateway_session_key3  s~     K%2&,,S1u:>U1X0E!H4FaqQx

 u:> %aF9r'   c                   | j                  |      }t        |j                  xs d      j                         }|r|j	                  d      dk7  s|sy|j	                  dd      }|j	                  dd      }|dk(  rt        |      xr ||k(  S |dv rxt        |j                  xs |j                  xs d      j                         }|r||k7  ryt        |j	                  d	d            j                         }t        |      xr ||k(  S y)
zBAuthorize approval/update interactions against session + operator.r   r  qqbotFr  ry  c2c>   groupguildr  )r  r    r  r   r   rx   group_openidguild_id)	r$   r{  r  r  operatorr  ry  
event_chatsession_users	            r&   &_is_authorized_interaction_for_sessionz0QQAdapter._is_authorized_interaction_for_sessionB  s     00=u,,2399;J/7:(JJ{B/	**Y+=8X%88**U//G5>>GRHNNPJw!6vzz)R89??AL%B(l*BBr'   c                  K   |j                   }|syt        |      }||\  }}| j                  j                  |      }|#t        j                  d| j                  ||       y| j                  ||      s-t        j                  d| j                  ||j                         y	 ddl	m
}  |||      }t        j                  d| j                  ||||j                         yt        |      }
|
d|j                   d	|j                   xs |j"                  xs |j$                   }| j                  ||      s,t        j                  d
| j                  |j                         y| j'                  |
|j                         yt        j)                  d| j                  ||j*                         y# t        $ r,}	t        j                  d| j                  ||	       Y d}	~	yd}	~	ww xY ww)u  Route ``INTERACTION_CREATE`` button clicks to the right subsystem.

        - ``approve:<session_key>:<decision>`` →
          :func:`tools.approval.resolve_gateway_approval`
          (unblocks the agent thread waiting on a dangerous-command approval).
        - ``update_prompt:<answer>`` →
          writes the answer to ``~/.hermes/.update_response`` for the
          detached ``hermes update --gateway`` process to consume.
        - Anything else is logged at DEBUG and ignored.

        Installed as the adapter's default interaction callback in
        ``__init__``. Callers can replace via
        :meth:`set_interaction_callback` to route clicks elsewhere (or pass
        ``None`` to drop them entirely).
        Nz.[%s] Unknown approval decision %r (session=%s)zF[%s] Rejected unauthorized approval click for session %s (operator=%s)r   )resolve_gateway_approvalzK[%s] Button resolved %d approval(s) for session %s (choice=%s, operator=%s)z7[%s] resolve_gateway_approval failed for session %s: %szagent:main:qqbot:r  z<[%s] Rejected unauthorized update prompt click (operator=%s)z4[%s] Unrecognised button_data %r from interaction %s)r  rO   _APPROVAL_BUTTON_TO_CHOICEr   r   r   rd   r  r  tools.approvalr  r   r   r   rQ   r  r  r  user_openid_write_update_responser<  r}  )r$   r{  r  approvalr  decisionchoicer  countr   update_answerupdate_session_keys               r&   r   z'QQAdapter._default_interaction_dispatch[  s    & ''-k:$,!K4488BF~DMM8[ >>ukR$MM;0E0E
  D0fE/MM5+v))	 7D$#4U[[M5CUCUC|Y^YgYgC|kpk|k|B}!~>>uFXYRMM5#8#8 ''u7L7LMBMM;	
'  MMM;  s1   B!G'$=F/ !CG'/	G$8"GG'G$$G'c                   	 ddl m}  |       }|dz  }|j                  d      }|j                  |        |j	                  |       t
        j                  d| |xs d       y# t        $ r }t
        j                  d|       Y d}~yd}~ww xY w)	a~  Atomically write the update-prompt answer to ``.update_response``.

        Mirrors the Discord / Telegram / Feishu adapters: the detached
        ``hermes update --gateway`` watcher polls this file for a ``y``/``n``
        response to its interactive prompts (stash-restore, config migration).
        Writes via ``tmp + rename`` so a partial write can't fool the reader.
        r   )get_hermes_homez.update_responsez.tmpz"QQ update prompt answered %r by %sz	(unknown)z#Failed to write update response: %sN)	hermes_constantsr  with_suffix
write_textreplacer   r   r   r   )answerr  r  homeresponse_pathtmpr   s          r&   r  z QQAdapter._write_update_response  s    	E8"$D #55M++F3CNN6"KK&KK4/K  	ELL>DD	Es   A A# #	B,BBc                t  K   t        |j                  dd            }|sy| j                  |      sy|}|j                  d      }t        j	                  d| j
                  ||r|dd nd|r t        |t              rt        |      nd dnd	       |rt        |t              rt        |      D ]v  \  }	}
t        |
t              st        j	                  d
| j
                  |	|
j                  dd      t        |
j                  dd            dd |
j                  dd             x | j                  |       d{   }|d   }|d   }|d   }|d   }|r9dj                  |      }|j                         r|dz   |z   j                         n|}|r(|j                         r|dz   |z   j                         n|}t        j	                  d| j
                  t        |      t        |             | j                  |       d{   }| j                  ||d         }|d   r||d   z   }||d   z   }|j                         s|syd| j                   |<   t#        | j%                  ||d      || j'                  ||      ||||| j)                  |            }| j+                  |       d{    y7 u7 7 
w)z%Handle a C2C (private) message event.r  r   Nattachmentsz1[%s] C2C message: id=%s content=%r attachments=%sr^   r   z itemsNonez7[%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quote_blockr  dmry  r  r  rx  r  message_typeraw_messagerw  
media_urlsmedia_typesr~  )r    r   _is_dm_intake_allowedr   r   rd   r`  listr(  	enumeratera  _process_attachmentsjoinr   _process_quoted_context_merge_quote_intor   r   build_source_detect_message_type_parse_qq_timestamprz  )r$   r:  rr  r  r  r~  r  r  attachments_raw_i_att
att_resultr  r  r  r  voice_blockquotedr{  s                      r&   r  zQQAdapter._handle_c2c_message  s     &**]B78))+6%%.?MM#GCRL # ,6ot+L3'RSTTZ[
	
 z/4@%o6 	DdD)KKQ4DHHUB/0"5R0	  44_EE
-
&':;&':;$%67 ))$56K9=,335;   ::< 0779$  	8MM
O!"		
 33A66%%dF=,AB,#f\&::J 1F;N4O Ozz|J+0K($$## % 
 22:?PQ!)..y9
 !!%(((i F8 70 	)s?   CJ8A6J8<J1=CJ8J4B(J8+J6,J84J86J8c                  K   t        |j                  dd            }|sy| j                  |t        |j                  dd                  sy| j                  |      }| j	                  |j                  d             d{   }|d   }	|d   }
|d   }|d	   }|r9d
j                  |      }|j                         r|dz   |z   j                         n|}|r(|j                         r|dz   |z   j                         n|}| j                  |       d{   }| j                  ||d         }|d   r|	|d   z   }	|
|d   z   }
|j                         s|	syd| j                  |<   t        | j                  |t        |j                  dd            d      || j                  |	|
      |||	|
| j                  |            }| j                  |       d{    y7 Z7 7 
w)zHandle a group @-message event.r  r   Nmember_openidr  r  r  r  r  r  r  r  r  r  r  )r    r   _is_group_allowed_strip_at_mentionr  r  r   r  r  r   r   r  r  r  rz  )r$   r:  rr  r  r  r~  r  r  r  r  r  r  r  r  r  r{  s                   r&   r  zQQAdapter._handle_group_message  s     15545%%c&**_b"AB
  %%g.44QUU=5IJJ
-
&':;&':;$%67 ))$56K9=,335;   ::< 0779$  33A66%%dF=,AB,#f\&::J 1F;N4O Ozz|J,3L)$$$FJJ;<! % 
 22:?PQ!)..y9
 !!%(((Y K( 70 	)s8   A?G"GBG"GCG"G G"G" G"c                  K   t        |j                  dd            }|syt        |j                  dd            }t        |j                  dd            }| j                  |xs ||      s#t        j	                  d| j
                  ||       yt        |j                  d      t              r|j                  d      ni }	t        |	j                  dd            xs t        |j                  d	d            }
|}| j                  |j                  d
             d{   }|d   }|d   }|d   }|d   }|r9dj                  |      }|j                         r|dz   |z   j                         n|}|r(|j                         r|dz   |z   j                         n|}| j                  |       d{   }| j                  ||d         }|d   r||d   z   }||d   z   }|j                         s|syd| j                  |<   t        | j                  |t        |j                  dd            |
xs dd      || j!                  ||      ||||| j#                  |            }| j%                  |       d{    y7 _7 7 
w)z%Handle a guild/channel message event.
channel_idr   Nr  r}  z5[%s] Guild message blocked by ACL: channel=%s user=%smembernickusernamer  r  r  r  r  r  r  r  r  r  )ry  r  	user_namer  r  )r    r   r  r   r<  rd   r`  ra  r  r  r   r  r  r   r   r  r  r  rz  )r$   r:  rr  r  r  r~  r  r  	author_idr  r  r  r  r  r  r  r  r  r  r{  s                       r&   r  zQQAdapter._handle_guild_message]  s     |R01

 quuZ,-

4,-	%%h&<*iHLLGz9 $.quuX$Ex26::fb)*Mc&**Z2L.M44QUU=5IJJ
-
&':;&':;$%67))$56K9=,335;   ::< 0779$  33A66%%dF=,AB,#f\&::J 1F;N4O Ozz|J*1J'$$"FJJtR01,$!	 %  22:?PQ!)..y9
 !!%(((Y K& 72 	)s8   DJI> BJ1J2CJ8J9JJJc                  K   t        |j                  dd            }|syt        |j                  dd            }| j                  |      s#t        j	                  d| j
                  ||       y|}| j                  |j                  d             d{   }	|	d   }
|	d   }|	d	   }|	d
   }|r9dj                  |      }|j                         r|dz   |z   j                         n|}|r(|j                         r|dz   |z   j                         n|}| j                  |       d{   }| j                  ||d         }|d   r|
|d   z   }
||d   z   }|j                         s|
syd| j                  |<   t        | j                  |t        |j                  dd            d      || j                  |
|      |||
|| j                  |            }| j!                  |       d{    y7 Z7 7 
w)z Handle a guild DM message event.r  r   Nr}  z.[%s] Guild DM blocked by ACL: guild=%s user=%sr  r  r  r  r  r  r  r  r  r  r  )r    r   r  r   r<  rd   r  r  r   r  r  r   r   r  r  r  rz  )r$   r:  rr  r  r  r~  r  r  r  r  r  r  r  r  r  r  r{  s                    r&   r  zQQAdapter._handle_dm_message  s     quuZ,-
 

4,-	)))4LL@x 44QUU=5IJJ
-
&':;&':;$%67))$56K9=,335;   ::< 0779$  33A66%%dF=,AB,#f\&::J 1F;N4O Ozz|J(,H%$$ FJJtR01 % 
 22:?PQ!)..y9
 !!%(((W K& 70 	)s8   BG6G/BG6'G2(CG6)G4*G62G64G6c                  K   dg g d}	 t        |j                  dd      xs d      dk7  r|S 	 |j                  d      }t	        |t
              r|s|S g }g }|D ]  }t	        |t              st        |j                  dd            j                         }|r|j                  |       |j                  d      }t	        |t
              sr|D ]$  }	t	        |	t              s|j                  |	       &  | j                  |       d	{   }
|
j                  d
      xs g }|
j                  d      xs d}|
j                  d      xs g }|
j                  d      xs g }g }|r |j                  dj                  |             |D ]  }|j                  |        |r|j                  |       |s|s|S |rddj                  |      z   }nd}|||dS # t        t        f$ r |cY S w xY w7 w)u:  Process the quoted message a user is replying to.

        When a user replies while quoting another message, the platform sets
        ``message_type = 103`` and pushes the referenced message's content and
        attachments inside ``msg_elements[0]``. The old adapter ignored
        ``msg_elements`` entirely, so:

        - Quoted text was surfaced only when the user typed something of
          their own — bare quote-replies showed nothing.
        - Quoted attachments (images, voice, files) were never downloaded
          or described.
        - Quoted voice messages specifically produced no transcript, so the
          LLM had no way to see what the user was referring to.

        This method parses ``msg_elements`` and runs the quoted attachments
        through the same :meth:`_process_attachments` pipeline as the main
        message body, so quoted voice messages get STT transcripts and
        quoted images are cached identically.

        :param d: Raw inbound message dict (from the WS dispatch payload).
        :returns: Dict with keys:

            - ``quote_block``: string to prepend to the user's text body
              (empty when there's nothing quoted).
            - ``image_urls``: list of cached quoted-image paths.
            - ``image_media_types``: parallel list of image MIME types.
        r   )r  r  r  r  r   g   msg_elementsr  r  Nr  r  r  r   z[Quoted message]:
r  z[Quoted message]: (image))r   r   	TypeError
ValueErrorr`  r  ra  r    r   appendr  r  )r$   r:  emptyelementsquoted_text_partsall_attachmentselemetexteattsar  quoted_voicequoted_infoquoted_imagesquoted_image_typeslinesrQ  r  s                     r&   r  z!QQAdapter._process_quoted_context  s    @ !#
	155+0q1S8 9
 55((D)L
 (*02 
	2DdD)B/0668E!((/HH]+E%& 2A!!T*'..q12
	2  44_EE
!~~&9:@b nn%67=2"|4:'^^,?@FBLL"345 	ALLO	LL%]L/$))E2BBK 6K ''!3
 	
a :& 	L	0 FsA   G1#G BG1G1")G1G/CG1G,)G1+G,,G1c                X    |s| S | j                         r| d|  j                         S |S )z=Prepend ``quote_block`` to *text*, separated by a blank line.r  )r   )r  r  s     r&   r  zQQAdapter._merge_quote_intoP  s5     K::<!]$tf-3355r'   c                T   | st         j                  S |st         j                  S |r|d   j                         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        j                  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&   r  zQQAdapter._detect_message_type]  s     ###$$$/:[^))+
j Gz$9Vz=Q$$$j $$$j Gz$9$$$A	
 r'   c           	       K   t        |t              sg g g ddS g }g }g }g }|D ]  }t        |t              st        |j	                  dd            j                         j                         }t        |j	                  dd            j                         }t        |j	                  dd            }	|j                  d      rd| }
n|r|}
nd}
t        j                  d| j                  ||
d	d
 |	       | j                  ||	      r*t        |j	                  d      t              r)t        |j	                  dd            j                         nd}t        |j	                  d      t              r)t        |j	                  dd            j                         nd}| j                  |
||	|xs d	|xs d	       d	{   }|r7|j                  d|        t        j                  d| j                  |       t        j                  d| j                  |
d	d        |j                  d       |j                  d      r	 | j                  |
||	       d	{   }|rFt         j"                  j%                  |      r'|j                  |       |j                  |xs d       n#|r!t        j                  d| j                  |       	 | j                  |
||	       d	{   }|rH|	xs |}|j                  d      r|j                  d| d| d       n|j                  d| d| d        |rdj)                  |      nd}||||dS 7 7 # t&        $ r,}t        j                  d| j                  |       Y d	}~fd	}~ww xY w7 # t&        $ r,}t        j                  d| j                  |       Y d	}~d	}~ww xY ww)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   )r  r  r  r  r  r   r  //https:z@[%s] Processing attachment: content_type=%s, url=%s, filename=%sNr  asr_refer_textvoice_wav_urlr   r!  z[Voice] z[%s] Voice transcript: %sz[%s] Voice STT failed for %sr]   u   [Voice] [语音识别失败]image/z
image/jpegz)[%s] Cached image path does not exist: %sz[%s] Failed to cache image: %szvideo/z[video:  (z)]z[file: z#[%s] Failed to cache attachment: %sr  )r`  r  ra  r    r   r   r   
startswithr   r<  rd   _is_voice_content_type_stt_voice_attachmentr  r   _download_and_cacher   pathisfiler   r  )r$   r  r  r  r  other_attachmentsattcturl_rawr  r   	asr_referr!  
transcriptcached_pathr   r   r  s                     r&   r  zQQAdapter._process_attachmentsq  s     +t, %'%'#%	  !#
')')') K	\Cc4(SWW^R01779??AB#''%,-335G377:r23H!!$'wi(LLRCR **2x8 "#''*:";SA  0"56<<>  "#''/":C@ 45;;=  $(#=#=#,#4"/"74 $> $ 
 %,,x
|-DELL!<dmmZXNN#A4==RUVYWYRZ[%,,-KLx(W(,(@(@b((S"SK"rww~~k'B"))+6)001C|D$G MM'	\(,(@(@b((S"SK"'~2==2-44xvR}TV5WX-44wtfB{mSU5VWSK	\Z ;L$))$56QS$!2!2.	
 	
U  #T ! WLL!A4==RUVVW
 #T ! \LL!FWZ[[\s   F?OMBOMMA/MON'N(AN6 OM	N#!N
O
NON	O	!O>OO		Oc                  K   ddl m}  ||      st        d|dd        | j                  sy	 | j                  j	                  |d| j                                d{   }|j                          |j                  }|j                  d	      r%t        j                  |      xs d
}t        ||      S |dk(  s|j                  d      r| j!                  ||       d{   S |xs, t#        t%        |      j&                        j(                  xs d}	t+        ||	      S 7 # t        $ r/}t        j                  d| j                  |dd |       Y d}~yd}~ww xY w7 }w)zDownload a URL and cache it locally.

        :param original_name: Preferred filename from attachment metadata.
            Falls back to the URL path basename if empty.
        r   is_safe_urlzBlocked unsafe URL: Nr  r   )r   r   z[%s] Download failed for %s: %sr#  z.jpgr  audio/qq_attachment)tools.url_safetyr4  r  r   r   _qq_media_headersr   r  r   r   r<  rd   r%  	mimetypesguess_extensionr   _convert_audio_to_wavr   r   r)  r   r   )
r$   r   r  original_namer4  r   r   r   extr  s
             r&   r(  zQQAdapter._download_and_cache  sc     	133CH:>??  	**....0 /  D
 !!#<<D ""8,++L9CVC)$44W$(?(?(I 33D#>>>  #**+00#" 
 -T8<<5  	LL14==#cr(C 		 ?sN   -E!/D$ D"  D$  A!E!!E"A E!"D$ $	E-%EE!EE!c                    | j                         j                         }|j                         j                         |dk(  s|j                  d      ryd}t        fd|D              ryy)z0Check if an attachment is a voice/audio message.r  r5  T)	.silk.amr.mp3.wav.ogg.m4a.aacz.speex.flacc              3  @   K   | ]  }j                  |        y wN)endswith).0r=  fns     r&   	<genexpr>z3QQAdapter._is_voice_content_type.<locals>.<genexpr>!  s     =Cr{{3=s   F)r   r   r%  any)r  r  r-  _VOICE_EXTENSIONSrK  s       @r&   r&  z QQAdapter._is_voice_content_type  se     !'')^^##%=BMM(3

 =+<==r'   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   ru   s    r&   r8  zQQAdapter._qq_media_headers%  s+     #vd.@.@-A%BCC	r'   Nr"  c          	       K   |r&t         j                  d| j                  |dd        |S |}d}|r:|j                  d      rd| }|}d}t         j                  d| j                         d	d
lm}  ||      st         j                  d|dd        y	 | j                  s!t         j                  d| j                         y| j                         }	t         j                  d| j                  |dd |t        |	             | j                  j                  |d|	d       d{   }
|
j                          |
j                  }t         j                  d| j                  t        |      |
j                  j                  dd             t        |      dk  r+t         j                  d| j                  t        |             y|rhd	dl}|j!                  dd      5 }|j#                  |       |j$                  }ddd       t         j                  d| j                  t        |             nwt         j                  d| j                  |       | j'                  ||       d{   }|rt)        |      j+                         s!t         j                  d| j                         yt         j                  d| j                         | j-                  |       d{   }	 t/        j0                  |       |r&t         j                  d| j                  |dd        |S t         j                  d| j                         |S 7 # 1 sw Y   IxY w7 7 v# t2        $ r Y iw xY w# t4        j6                  t4        j8                  t:        f$ r?}t         j                  d| j                  t=        |      j>                  |       Y d}~yd}~ww xY w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   r3  z[QQ] STT blocked unsafe URL: %sr  z[%s] STT: no HTTP clientz<[%s] STT: downloading voice from %s (pre_wav=%s, headers=%s)r   )r   r   r   z.[%s] STT: downloaded %d bytes, content_type=%szcontent-typeunknownrS  z8[%s] STT: downloaded data too small (%d bytes), skippingrB  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<  rd   r%  r7  r4  r   r   r8  rx   r   r   r  r(  r   tempfileNamedTemporaryFilewriter   _convert_audio_to_wav_filer   exists	_call_sttr   unlinkOSErrorr   HTTPStatusErrorTransportErrorIOErrorr,  r)   )r$   r   r  r  r   r!  download_url
is_pre_wavr4  download_headersr   
audio_datarV  r  wav_pathr0  r   s                    r&   r'  zQQAdapter._stt_voice_attachment0  sY    & LL7W[X[H\ "! 
''-"( 8(LJLLLdmm\0<(NN<l3B>OPR	$$94==I#557LLNSb!%& **..(!%	 /  D !!#JLL@J  ;	 :#NMM
O
  00u0M (QTIIj)"xxH( KMM
O >x "&!@!@X!VVtH~'<'<'>NNH$--   LL6xP#~~h77J		(# 3T]]JtPSDTU  H$--Xy6( ( W 8
   %%u';';WE 	NN>S	""	 	s   BO	,M  O	A!M "L<#BM  O	M L?8A)M !M"?M !O	"5M MM M 2'M O	!M ;O	<M ?M		M M 	MM MM (O5O<O	OO	c           	       K   ddl }t        |      j                  r#t        |      j                  j                         n| j	                  |      }t
        j                  d| j                  t        |      ||dd        |j                  |d      5 }|j                  |       |j                  }ddd       j                  dd      d   d	z   }| j                  ||       d{   }|s| j                  ||       d{   }|s| j                  ||       d{   }	 t!        j"                  |       |S # 1 sw Y   xY w7 `7 F7 ,# t$        $ r Y |S w xY ww)
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   FrS  .r   rB  )rV  r   rT  r   _guess_ext_from_datar   r   rd   r(  rW  rX  r   rsplit_convert_silk_to_wav_convert_ffmpeg_to_wav_convert_raw_to_wavr   r\  r]  )	r$   rd  r  rV  r=  tmp_srcsrc_pathre  results	            r&   rY  z$QQAdapter._convert_audio_to_wav_file  s\     	 H~$$ N!!'')**:6 	
 	EMM
OsO	
 ((E(B 	$gMM*%||H	$ ??3*1-6 008DD 66xJJF 33JIIF	IIh /	$ 	$ E K J
  		sr   BED:05E%E&EEEE
E#E 8E:E?EE
E	EEEEc                    | dd dk(  s| dd dk(  ry| dd dk(  ry| dd	 d
k(  ry| dd	 dk(  ry| dd dv ry| dd	 dk(  s| dd	 dk(  ry| dd	 dk(  s| dd	 dk(  ryy)z&Guess file extension from magic bytes.Nr_  	   #!SILK_V3rF     #!SILKr?  r>     !rm  s   RIFFrB  s   fLaCrF  >            rA  s   0&us   OggSrC  s       s      r@  rU   r   s    r&   ri  zQQAdapter._guess_ext_from_data  s     8|#tBQx9'<8x8w8w8>>8**d2Ah:M.M8**d2Ah:M.Mr'   c                :    | dd dk(  xs | dd dk(  xs | dd dk(  S )z+Check if bytes look like a SILK audio file.NrF  rs  r>  rt  r_  rr  rU   rx  s    r&   _looks_like_silkzQQAdapter._looks_like_silk  s5     BQx9$XRaH(<XRaL@XXr'   c                F  K   	 ddl }	 |j                  ||d       t        |      j                         r~t        |      j                         j                  dkD  rXt        j                  d| j                  t        |      j                  t        |      j                         j                         |S |j                  d	d
      d   dz   }	 ddl}|j                  ||       |j                  ||d       t        |      j                         rt        |      j                         j                  dkD  rnt        j                  d| j                  t        |      j                  t        |      j                         j                         |	 t!        j"                  |       S 	 t!        j"                  |       y# t        $ r# t        j                  d| j                         Y yw xY w# t        $ r,}t        j                  d| j                  |       Y d}~fd}~ww xY w# t$        $ r Y S w xY w# t        $ r+}t        j                  d| j                  |       Y d}~d}~ww xY w# t$        $ r Y yw xY w# 	 t!        j"                  |       w # t$        $ r Y w w xY wxY ww)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: %srh  r   r?  z3[%s] pilk converted %s (as .silk) to wav (%d bytes)z%[%s] pilk .silk conversion failed: %s)pilkImportErrorr   r   rd   silk_to_wavr   rZ  statst_sizer<  r   r   rj  shutilcopy2r   r\  r]  )r$   ro  re  r  r   	silk_pathr  s          r&   rk  zQQAdapter._convert_silk_to_wav  s9    		WXxe<H~$$&4>+>+>+@+H+H2+M>MMN''N'')11	  
 OOC+A.8		LL9-Yu=H~$$&4>+>+>+@+H+H2+MIMMN''N'')11	  		)$		)$ Y  	NN] 	$  	WLLA4==RUVV	W.    	VLL@$--QTUU	V
  		)$ s   J!F; B*G* 3J!C H1 H"#J!%I( :J!;)G'$J!&G''J!*	H3!HJ!HJ!"	H.+J!-H..J!1	I%:!I I7  I%%I7 (	I41J!3I44J!7J9JJ	JJJJJ!c                ^  K   	 ddl }|j                  |d      5 }|j                  d       |j                  d       |j	                  d       |j                  |       ddd       |S # 1 sw Y   |S xY w# t        $ r+}t        j                  d| j                  |       Y d}~yd}~ww xY w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)
waveopensetnchannelssetsampwidthsetframeratewriteframesr   r   r<  rd   )r$   rd  re  r  wfr   s         r&   rm  zQQAdapter._convert_raw_to_wav-  s     	8S) +R""&z*	+
 O+
 O 	LL;T]]CP	sR   B-A6 AA)	A6 (B-)A3.A6 2B-3A6 6	B*?!B% B-%B**B-c                `  K   	 t        j                  ddd|dddd|t         j                  j                  t         j                  j                         d	{   }t        j
                  |j                         d
       d	{    |j                  dk7  ry|j                  r"|j                  j                          d	{   nd}t        j                  d| j                  t        |      j                  |d	d j                  d             y		 t        |      j%                         r&t        |      j'                         j(                  dk  r5t        j                  d| j                  t        |      j                         y	t        j+                  d| j                  t        |      j                  t        |      j'                         j(                         |S 7 7 ]7 ## t         j                   t"        f$ r+}t        j                  d| j                  |       Y d	}~y	d	}~ww xY ww)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: %sr  r  )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   rd   r   r   decodeTimeoutErrorFileNotFoundErrorrZ  r  r  r<  )r$   ro  re  procr  r   s         r&   rl  z QQAdapter._convert_ffmpeg_to_wav@  s    	 77))11)).. D ""499;;;;!#59[[t{{//111c3MMN''4CL''y'9	  $ H~$$&$x.*=*=*?*G*G2*MNN=X##
 8MMNN!))		
 Q <1 $$&78 	NN=t}}cR	sg   H.AG' G,G' G!<G' G$AG' CH.G' !G' $G' 'H+ !H&!H.&H++H.c                   | j                   j                  xs i }|j                  d      }t        |t              r|j                  d      dur|j                  d      xs |j                  dd      }|j                  d      xs |j                  dd      }|j                  d	d      }|r|r|j                  d
      ||xs ddS |r>|j                  dd      }dddd}|j                  |d      }|r|||xs
 |dv rddS ddS t        j                  dd      }|rAt        j                  dd      }t        j                  dd      }|j                  d
      ||dS y)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)r   r   r   r`  ra  rstripr   r   )	r$   r   stt_cfgr  r  r  r  _PROVIDER_BASE_URLS
qq_stt_keys	            r&   _resolve_stt_configzQQAdapter._resolve_stt_configm  s    !!'R ))E"gt$Y)?u)L{{9-LZ1LHkk(+Iw{{9b/IGKK,EG ( 4&"1k  ";;z59 I9H'#
 /228R@$,#*!& "_2:n2LY	  S^	  YY/4
yy!=H IIni8E$OOC0%  r'   c           	     Z  K   | j                         }|s!t        j                  d| j                         y|d   }|d   }|d   }	 t	        |d      5 }| j
                  j                  | ddd	| id
t        |      j                  |dfid|id       d{   }ddd       j                          |j                         }|j                  dg       }	|	rE|	d   j                  di       j                  dd      }
|
j                         r|
j                         S |j                  dd      }|j                         r|j                         S y7 # 1 sw Y   xY w# t        j                  t        f$ r0}t        j                  d| j                  ||dd |       Y d}~yd}~ww xY w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/wavr   )r   filesr   r   choicesr   r   r  r   r  z0[%s] STT API call failed (model=%s, base=%s): %sr^   )r  r   r   rd   r  r   r   r   r   r   r   r   r   r   r^  r`  )r$   re  r  r  r  r  fr   rp  r  r  r  r   s                r&   r[  zQQAdapter._call_stt  s     **,NNK :&)$ 	h% !..33j 56,y.AB!DN$7$7K#HI!5)  4   !!#YY[FjjB/G!!*..B7;;IrJ==?"==?*::fb)Dzz|zz|#' * %%w/ 	NNB" 	sh   AF+E AEEEB E F+1E F+EEE F(8&F#F+#F((F+c                  K   ddl }t        |      j                  r6t        t        |      j                        j                  j                         nd}|r|dvr| j                  |      }|j                  |d      5 }|j                  |       |j                  }ddd       j                  dd      d   d	z   }	 |d
k(  xs | j                  |      }|r| j                  ||       d{   }	n| j                  ||       d{   }	|	sJt        j                  d| j                   |dd |       t#        |d|       	 t%        j&                  |       S 	 	 t%        j&                  |       	 t        |      j-                         }
t%        j&                  |       t#        |
d      S # 1 sw Y   xY w7 7 # t(        $ r Y S w xY w# t*        $ r7 t#        |d|       cY 	 t%        j&                  |       S # t(        $ r Y S w xY ww xY w# t(        $ r Y w xY w# 	 t%        j&                  |       w # t(        $ r Y w w xY wxY w# t*        $ r+}t        j/                  d| j                   |       Y d}~yd}~ww xY ww)zLConvert audio bytes to .wav using pilk (SILK) or ffmpeg, caching the result.r   Nr   >   rE  r@  rD  rA  rC  rB  rF  r?  FrS  rh  r   rB  r?  z/[%s] audio conversion failed for %s (format=%s)r]   qq_voicezqq_voice.wavz%[%s] Failed to read converted wav: %s)rV  r   r)  r   rT  r   ri  rW  rX  r   rj  rz  rk  rl  r   r   rd   r   r   r\  r]  r   
read_bytesr<  )r$   rd  
source_urlrV  r=  rn  ro  re  is_silkrp  wav_datar   s               r&   r;  zQQAdapter._convert_audio_to_wav  sf     	
 
#(( *%**+2288: 	
 c 	"
 	
 ++J7C((E(B 	$gMM*%||H	$ ??3*1-6	WnI(=(=j(IG#888LL#::8XNNEMMsO	 1xu=MN		(# 		(#
	H~002HIIh,X~FFA	$ 	$ MN    	K,Z8C59IJJ		(# 	K
  		(#   	LL@$--QTU	s   A<I=>F* I==/G
 ,F7-G
 F99G
 F;I=H 09I )I=*F4/I=7G
 9G
 ;	GI=GI=
H
"H $G:9I=:	HI=HI=	H

H 	HI=HI=IH43I4	I =I?I  II=	I:!I50I=5I::I=c                  K   | j                   st        d      | j                          d{   }d| dt               d}	 | j                   j	                  |t
         | |||       d{   }|j                         }|j                  dk\  r.t        d|j                   d	| d
|j                  d|             |S 7 7 U# t        j                  $ r}	t        d| d|	       |	d}	~	ww xY ww)z5Make an authenticated REST API request to QQ Bot API.r  Nr   r  r  r  r  zQQ Bot API error [z] z: r   zQQ Bot API timeout [r  )r   rj   r   rF   requestr/   r   r  r   r   TimeoutException)
r$   methodr)  bodyr   r   r   r   r   r   s
             r&   _api_requestzQQAdapter._api_request	  s        OPP((**%eW-.*,
	O**22*TF# 3  D 99;D3&"()9)9(:"TF"xx	4013  K+ + %% 	O!5dV3seDE3N	OsF   +C4CC4+C /C0AC C4C C1C,,C11C4c                  K   |dk(  rd| dnd| d}||d}	|r||	d<   n|r||	d<   |t         k(  r|r||	d<   t        d	      D ]%  }
	 | j                  d
||	t               d{   c S  y7 # t        $ rU}t        |      t        fddD              r |
dk  r%t        j                  d|
dz   z         d{  7   n Y d}~d}~ww xY ww)z"Upload media and return file_info.r  
/v2/users/z/files/v2/groups/)	file_typesrv_send_msgr   	file_data	file_namer  POSTr  Nc              3  &   K   | ]  }|v  
 y wrH  rU   )rJ  kwerr_msgs     r&   rL  z*QQAdapter._upload_media.<locals>.<genexpr>a	  s       g   )400401Invalidr   Timeoutr>  g      ?r   )	rD   ranger  r3   rj   r    rM  r   r  )r$   target_type	target_idr  r   r  r  r  r)  r  attemptr   r  s               @r&   _upload_mediazQQAdapter._upload_media=	  s     e# 6*yk0 	 #( 
 DK )D'I )D Qx 	G!..D$0C /   	   
c( "Q  Q;!--w{(;<<< =
sO   ACA/$A-%A/(C-A//	C8AC;B><CCCCg      .@g      ?c                  K   t         j                  d| j                  | j                         d}|| j                  k  rtt	        j
                  | j                         d{    || j                  z  }| j                  r"t         j                  d| j                  |       y|| j                  k  rtt         j                  d| j                  | j                         y7 }w)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)r   Nz[%s] Reconnected after %.1fsTz$[%s] Still not connected after %.0fsF)	r   r   rd   _RECONNECT_WAIT_SECONDSr   r  _RECONNECT_POLL_INTERVALr|   r   )r$   waiteds     r&   _wait_for_reconnectionz QQAdapter._wait_for_reconnectionp	  s      	SMM4#?#?	At333-- = =>>>d333F  :DMM6R t333 	=t}}dNjNjk ?s   AC!!C"AC!3-C!c                  K   ~| j                   s&| j                          d{   st        ddd      S |r|j                         st        d      S | j	                  |      }| j                  || j                        }t        dd      }|D ]/  }| j                  |||       d{   }|j                  s|c S d}1 |S 7 7 w)	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   )	r|   r  r   r   format_messagetruncate_messager:   _send_chunkr  )	r$   ry  r  reply_tometadata	formattedchunkslast_resultchunks	            r&   sendzQQAdapter.send	  s        44666!%RVWWgmmod++''0	&&y$2I2IJ kB 	E $ 0 0% JJK&&""H	 ! 7 Ks"   !CCBC)C*CCc           	     0  
K   d}| j                  |      }t        d      D ]z  }	 |dk(  r| j                  |||       d{   c S |dk(  r| j                  |||       d{   c S |dk(  r| j	                  |||       d{   c S t        dd|       c S  |rt        |      ndt        j                  d| j                         t        fddD               }	t        d|	      S 7 7 7 p# t        $ r}|}t        |      j                         
t        
fd	d
D              rY d}~ |dk  rLdd|z  z  }t        j                  d| j                  |dz   ||       t        j                  |       d{  7   Y d}~kd}~ww xY ww)z5Send a single chunk with retry + exponential backoff.Nr  r  r  r  FzUnknown chat type for r  c              3  &   K   | ]  }|v  
 y wrH  rU   )rJ  kerrs     r&   rL  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  B   K   | ]  }|j                         v   y wrH  )r   )rJ  r  	error_msgs     r&   rL  z(QQAdapter._send_chunk.<locals>.<genexpr>	  s"      
'(A""
s   )r  r  r  r  )_guess_chat_typer  _send_c2c_text_send_group_text_send_guild_textr   r   r    r   rM  r   r   rd   r   r  r   )r$   ry  r  r  last_excr  r  r   r)  r   r  r  s             @@r&   r  zQQAdapter._send_chunk	  s     )-))'2	Qx 	/G/%!%!4!4Wgx!PPP')!%!6!6w!RRR')!%!6!6w!RRR% %/EgY-O 	/B &.CM?	+T]]IF 
,Q
 
 
	 %yINNG QRR
  /#hnn& !U  Q;1<0ENN>! "--...'/s   "FC9C3C9F	C9$C5%C9(F+C9C7C9
FC9AF3C95C97C99	F/F1F7AFFFFFFc                b  K   | j                  |xs |       | j                  ||      }|r||d<   ||j                         |d<   | j                  dd| d|       d{   }t	        |j                  dt        j                         j                  dd             }t        d	||
      S 7 Lw)zzSend text to a C2C user via REST API.

        :param keyboard: Optional inline keyboard attached to the message.
        rr  Nkeyboardr  r  	/messagesr}     Tr  rw  raw_response
ru  _build_text_bodyto_dictr  r    r   ro  rp  rq  r   )r$   openidr  r  r  r  r   rr  s           r&   r  zQQAdapter._send_c2c_text	  s      	8-v.$$Wh7%DN'//1D&&vF89/MtTTTXXdDJJL$4$4Sb$9:;$6MM U   AB/ B-!AB/c                b  K   | j                  |xs |       | j                  ||      }|r||d<   ||j                         |d<   | j                  dd| d|       d{   }t	        |j                  dt        j                         j                  dd             }t        d	||
      S 7 Lw)zwSend text to a group via REST API.

        :param keyboard: Optional inline keyboard attached to the message.
        rr  Nr  r  r  r  r}  r  Tr  r	  )r$   r  r  r  r  r  r   rr  s           r&   r   zQQAdapter._send_group_text	  s      	83|4$$Wh7%DN'//1D&&k,y94
 
 TXXdDJJL$4$4Sb$9:;$6MM	
r  c                  K   d|d| j                    i}|r||d<   | j                  dd| d|       d{   }t        |j                  dt	        j
                         j                  dd             }t        d	||
      S 7 Lw)z*Send text to a guild channel via REST API.r  Nrr  r  z
/channels/r  r}  r  Tr  )r:   r  r    r   ro  rp  rq  r   )r$   r  r  r  r  r   rr  s          r&   r  zQQAdapter._send_guild_text
  s      !*73LT5L5L+MN%DN&&vJ<y/QSWXXTXXdDJJL$4$4Sb$9:;$6MM Ys   3BBABc                "  K   | j                   s&| j                          d{   st        ddd      S | j                  |      }| j	                  |      }|d| j
                   }	 |dk(  r| j                  ||||       d{   S |dk(  r| j                  ||||       d{   S t        dd	|d      S 7 7 97 # t        $ rA}t        j                  d
| j                  |       t        dt        |            cY d}~S d}~ww xY ww)u  Send a single text message with an inline keyboard attached.

        Unlike :meth:`send`, this does NOT split long content into chunks —
        a keyboard message has exactly one interactive surface, and splitting
        would orphan the buttons from the first chunk. Callers should keep
        approval/update-prompt bodies short.

        Guild (channel) chats don't support inline keyboards; returns a
        non-retryable failure for those.
        NFr  Tr  r  )r  r  z-Inline keyboards not supported for chat_type z"[%s] send_with_keyboard failed: %sr  )r|   r  r   r  r  r:   r  r   r   r   r   rd   r    )	r$   ry  r  r  r  r  r  	truncatedr   s	            r&   send_with_keyboardzQQAdapter.send_with_keyboard
  s?    "   44666!!D  ))'2	''0	7 7 78		=E!!00Y8 1    G#!22Y8 3    C m%   # 7  	=LL4dmmS e3s8<<		=s|    DB<AD'C B>C D	C &C 'C *D+C ;D>C  C 	D6DDDDDc                   K   ddl m} | j                  | ||      t        |j                  t        |dd            |       d{   S 7 w)u  Send a 3-button approval request (``allow-once / allow-always / deny``).

        The rendered text comes from :func:`build_approval_text`; callers can
        override by passing a custom :class:`ApprovalRequest`.

        Users click the button → ``INTERACTION_CREATE`` fires → the adapter's
        registered :meth:`set_interaction_callback` handler decodes
        ``button_data`` via :func:`parse_approval_button_data`.
        r   )build_approval_textallow_permanentT)r  r  N)!gateway.platforms.qqbot.keyboardsr  r  rM   r  rb   )r$   ry  reqr  r  s        r&   send_approval_requestzQQAdapter.send_approval_requestH
  sZ      	J,,$# '-> E  - 
 
 	
 
s   AAAAc           	        K   ~|r|dz  }| j                   j                  |      }t        |d||| j                  |xr |       }	| j	                  ||	|       d{   S 7 w)u|  Send a button-based exec-approval prompt for a dangerous command.

        Called by ``gateway/run.py``'s ``_approval_notify_sync`` when the
        agent is blocked waiting for approval. Button clicks resolve via
        :func:`tools.approval.resolve_gateway_approval` — dispatched by the
        adapter's interaction callback (:meth:`_default_interaction_dispatch`).
        z3 Owner override applies to this one operation only.zExecute this command?)r  titledescriptioncommand_previewtimeout_secr  r  N)r   r   rJ   _APPROVAL_TIMEOUT_SECONDSr  )
r$   ry  commandr  r  r  r  smart_deniedrr  r  s
             r&   send_exec_approvalzQQAdapter.send_exec_approvalk
  s     " PPK
 ""&&w/#)##66+@L0@
 //S6 0 
 
 	
 
s   AA$A"A$i,  c                   K   ~~|rd| dnd}d| | }| j                   j                  |      }| j                  ||t               |       d{   S 7 w)a  Send a Yes/No update-confirmation prompt with inline buttons.

        Matches the cross-adapter contract used by
        ``gateway/run.py``'s ``hermes update --gateway`` watcher. Button
        clicks surface as ``INTERACTION_CREATE`` with
        ``button_data = 'update_prompt:y'`` or ``'update_prompt:n'``;
        the adapter's interaction callback writes the answer to
        ``~/.hermes/.update_response`` so the detached update process
        can read it.
        z (default: r   r   u!   ⚕ **Update Needs Your Input**

r  N)r   r   r  rN   )	r$   ry  promptdefaultr  r  default_hintr  rr  s	            r&   send_update_promptzQQAdapter.send_update_prompt
  sv     $ 3:WIQ/7x~N""&&w/,,(*	 - 
 
 	
 
s   AAAAc                    | j                  |xs 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.r%  r  N)markdownmsg_typemsg_seq)r  r*  r+  rw  message_reference)ru  r   r:   r>   r=   )r$   r  r  r+  r  s        r&   r
  zQQAdapter._build_text_body
  s     $$X%:;!!&0I$2I2I(JK-"$D ##<T%<%<=)"D ))-98,D()r'   c                :  K   ~| j                  ||t        d||       d{   }|j                  s| j                  |      s|S t        j                  d| j                  |j                         |r| d| n|}| j                  |||       d{   S 7 u7 w)z-Send an image natively via QQ Bot API upload.r  Nz0[%s] Image send failed, falling back to text: %sr  )ry  r  r  )	_send_mediarA   r  _is_urlr   r   rd   r   r  )r$   ry  	image_urlcaptionr  r  rp  fallbacks           r&   
send_imagezQQAdapter.send_image
  s      ''Y 0'7H
 
 >>i!8M 	>MMLL	

 18gYb,YYYw8YTTT
 Us"   BBA0BBBBc                T   K   ~| j                  ||t        d||       d{   S 7 w)z!Send a local image file natively.r  N)r.  rA   )r$   ry  
image_pathr1  r  kwargss         r&   send_image_filezQQAdapter.send_image_file
  6      %%Z!17GX
 
 	
 
   (&(c                T   K   ~| j                  ||t        d||       d{   S 7 w)zSend a voice message natively.r  N)r.  rC   )r$   ry  
audio_pathr1  r  r6  s         r&   
send_voicezQQAdapter.send_voice
  r8  r9  c                T   K   ~| j                  ||t        d||       d{   S 7 w)zSend a video natively.r  N)r.  rB   )r$   ry  
video_pathr1  r  r6  s         r&   
send_videozQQAdapter.send_video  r8  r9  c           	     X   K   ~| j                  ||t        d|||       d{   S 7 w)zSend a file/document natively.r  )r  N)r.  rD   )r$   ry  	file_pathr1  r  r  r6  s          r&   send_documentzQQAdapter.send_document  sB      %% & 
 
 	
 
s   !*(*c                N  K   | j                   s&| j                          d{   st        ddd      S | j                  |      }|dk(  rt        dd      S 	 | j	                  |      r[|xs, t        t        |      j                        j                  xs d	}	| j                  ||||d|t        k(  r|	nd
       d{   }
n | j                  |||||       d{   \  }	}
|
j                  d      xs% |
j                  di       xs i j                  d      }|st        dd|
       S | j                  |      }t        d|i|d}|r|d| j                   |d<   |r||d<   | j!                  d|dk(  rd| dnd| d|       d{   }t        dt#        |j                  dt%        j&                         j(                  dd             |      S 7 7 7 7 R# t*        $ ri}t,        j/                  d| j0                  |j2                  |j4                         t        dd|j2                  d|j4                   dd      cY d}~S d}~wt6        $ r}t,        j/                  d| j0                  |j2                  |j4                  |j8                         t        d|j2                  d|j4                   d|j8                   dd      cY d}~S d}~wt:        $ rA}t,        j=                  d | j0                  |       t        dt#        |            cY d}~S d}~ww xY ww)!u  Upload media and send as a native message.

        Upload strategy:

        - **HTTP(S) URLs** → single ``POST /v2/{users|groups}/{id}/files``
          with ``url=...``. The QQ platform fetches the URL directly; fastest
          path when the source is already hosted.
        - **Local files** → three-step chunked upload (prepare / PUT parts /
          complete). Handles files up to the platform's ~100 MB per-file
          limit without the ~10 MB inline-base64 cap of the old adapter.
        NFr  Tr  r  z,Guild media send not supported via this pathr  media)r   r  r  	file_infor   zUpload returned no file_info: )r*  rD  r+  r  rr  r  r  r  r  r  r}  r  r  z,[%s] Daily upload limit exceeded for %s (%s)z#QQ daily upload limit exceeded for r$  z). Retry tomorrow.z/[%s] File too large: %s (%s, platform limit %s)z() exceeds the QQ per-file upload limit (z).z[%s] Media send failed: %s)r|   r  r   r  r/  r   r   r)  r   r  rD   _upload_local_filer   ru  r?   r:   r  r    ro  rp  rq  rH   r   r   rd   r  file_size_humanrI   limit_humanr   r   )r$   ry  media_sourcer  kindr1  r  r  r  resolved_nameuploadrE  r+  r  	send_datar   s                   r&   r.  zQQAdapter._send_media(  sX    *   44666!%RVWW))'2	D 
[	=||L)  H\2778== 
  $11$!&/8O/KmQU  2    /3.E.E / )%v 

;/ 

62&,"c+  !!:6(C  ((1G*%y1"$D
 ")*CD,C,C"DY!)X"// !E) !	3&wiy9 I y}}T4::<3C3CCR3HIJ& G 7&): - 	 NN>s}}c.A.A 9#--9J K++,,>@    ' 	NNAs}}c.A.A3?? }}'r#*=*=)> ?1140AE     	=LL5t}}cJe3s8<<	=s    L%G5L%A&G! ?G G! GAG! .L%/AG! GA	G! L%G! G! G! !	L"*AIL"	L%L"A5KL"L%L"!6LL"L%L""L%c                p  K   | j                   st        d      t        |      j                         }|j	                         s%t        j
                         |z  j                         }|j                         r|j                         s;|j                  d      st        |      dk  rt        d|      t        d|       |xs |j                  }t        | j                  | j                   j                   | j"                        }|j%                  ||t'        |      ||       d{   }	||	fS 7 w)	aN  Chunked-upload a local file and return ``(resolved_name, complete_response)``.

        The returned ``complete_response`` contains the ``file_info`` token
        that goes into the subsequent RichMedia message body.

        :raises UploadDailyLimitExceededError: On biz_code 40093002.
        :raises UploadFileTooLargeError: When the file exceeds the platform limit.
        :raises FileNotFoundError: If the path does not exist.
        :raises ValueError: If the path looks like a placeholder (``<path>``).
        :raises RuntimeError: If the HTTP client is not initialized.
        r  <r  1Invalid media source (looks like a placeholder): Media file not found: )api_requesthttp_putlog_tag)r  r  rA  r  r  N)r   rj   r   
expanduseris_absolutecwdresolverZ  is_filer%  r(  r  r  r   rG   r  r  rd   rL  r    )
r$   r  ry  rI  r  r  
local_pathrK  uploadercompletes
             r&   rF  zQQAdapter._upload_local_file  s)    &   OPP,'224
%%'((*z1::<J  "**<*<*>&&s+s</@1/D GGWX  $&<ZL$IJJ!4Z__"))&&**MM

 "*o# ) 
 
 h&&
s   D)D6+D4,	D6c                F  K   t        |      j                         }|st        d      t        |      }|j                  dv rHt        j                  |      d   xs d}|xs# t        |j                        j                  xs d}|||fS t        |      j                         }|j                         s%t        j                         |z  j                         }|j                         r|j                         s;|j!                  d      st#        |      dk  rt        d|      t%        d	|       |j'                         }|xs |j                  }t        j                  t        |            d   xs d}t)        j*                  |      j-                  d
      }|||fS w)zSLoad media from URL or local path. Returns (base64_or_url, content_type, filename).zMedia source is required>   httphttpsr   zapplication/octet-streamrD  rO  r  rP  rQ  ascii)r    r   r  r   schemer9  
guess_typer   r)  r   rU  rV  rW  rX  rZ  rY  r%  r(  r  r  base64	b64encoder  )	r$   rx  r  r  r  rK  rZ  rk  b64s	            r&   _load_mediazQQAdapter._load_media  s     V""$788&!==--$//7:X>XL%Jfkk):)?)?J7M<66 &\,,.
%%'((*z1::<J  "**<*<*>   %Vq GzR  $&<ZL$IJJ##%!4Z__$$S_5a8V<V 	 s#**73L-//s   FF!c                8  K   | j                   sy| j                  |      }|dk7  ry| j                  j                  |      }|syt	        j                         }| j
                  j                  |d      }||z
  | j                  k  ry	 | j                  |      }t        |d| j                  d|d}| j                  dd| d	|       d{    || j
                  |<   y7 # t        $ r+}	t        j                  d
| j                  |	       Y d}	~	yd}	~	ww xY w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``.
        Nr  r   r   )
input_typeinput_second)r*  rr  input_notifyr+  r  r  r  z[%s] send_typing failed: %s)r|   r  r   r   r   r   _TYPING_DEBOUNCE_SECONDSru  r@   _TYPING_INPUT_SECONDSr  r   r   r<  rd   )
r$   ry  r  r  rr  now	last_sentr+  r  r   s
             r&   send_typingzQQAdapter.send_typing   s        ))'2	""&&w/ iik((,,Wc:	?T:::	L((1G1 "#$($>$>! #D ##Fj	,KTRRR,/D  ) S 	LLL6sKK	LsC   BD	AC# C!C#  D!C# #	D,!DDDDc                4    | 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).
        )r   r   )r$   r  s     r&   r  zQQAdapter.format_message,  s     !!Ng&&r'   c                F   K   | j                  |      }||dv rddS ddS w)z/Return chat info based on chat type heuristics.>   r  r  r  r  )r   r,  )r  )r$   ry  r  s      r&   get_chat_infozQQAdapter.get_chat_info:  s;     ))'2	(,>>G
 	
DH
 	
s   !c                B    t        t        |             j                  dv S )N>   r^  r_  )r   r    ra  )rx  s    r&   r/  zQQAdapter._is_urlF  s    F$++/@@@r'   c                >    || j                   v r| j                   |   S y)zDDetermine chat type from stored inbound metadata, fallback to 'c2c'.r  )r   )r$   ry  s     r&   r  zQQAdapter._guess_chat_typeJ  s$    d)))&&w//r'   c                P    ddl }|j                  dd| j                               }|S )z9Strip the @bot mention prefix from group message content.r   Nz^@\S+\s*r   )resubr   )r  rv  strippeds      r&   r  zQQAdapter._strip_at_mentionP  s$     	66+r7==?;r'   c                    t        j                  dd      j                         dv ryt        j                  dd      j                         dv S )NGATEWAY_ALLOW_ALL_USERSr   >   r  yestrueTQQ_ALLOW_ALL_USERS)r   r   r   ru   s    r&   _open_dm_opted_inzQQAdapter._open_dm_opted_inY  sD    99.399;?SSyy-r288:>RRRr'   c                    | j                   dk(  ry| j                   dk(  r| j                  | j                  |      S | j                   dk(  r| j                         S y)NdisabledF	allowlistr  )r   _entry_matchesr   r~  )r$   r  s     r&   _is_dm_allowedzQQAdapter._is_dm_allowed^  sT    ??j(??k)&&t'7'7AA??f$))++r'   c                   t        |xs d      j                         }|sy| j                  dk(  ry| j                  dk(  r| j                  | j                  |      S | j                  dk(  ry| j                  dk(  r| j                         S y)Nr   Fr  r  r   Tr  )r    r   r   r  r   r~  )r$   r  	principals      r&   r  zQQAdapter._is_dm_intake_allowedg  s    2&,,.	??j(??k)&&t'7'7CC??i'??f$))++r'   c                    | j                   dk(  ry| j                   dk(  r| j                  | j                  |      S | j                   dk(  ry| j                   dk(  ryy)Nr  Fr  r   r  T)r   r  r   )r$   group_idr  s      r&   r  zQQAdapter._is_group_allowedu  s_    +,&&t'='=xHH*'r'   c                    t        |      j                         j                         }| D ]5  }t        |      j                         j                         }|dk(  s||k(  s5 y y)N*TF)r    r   r   )entriestargetnormalized_targetentry
normalizeds        r&   r  zQQAdapter._entry_matches  s`    K--/557 	EU))+113JS J2C$C	 r'   c                z   |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   rm  r   utcfromisoformatr  r  fromtimestampr   )r$   rk  s     r&   r  zQQAdapter._parse_qq_timestamp  s     <<8<<00	))#..I& 			))#c(T/hllKKI& 		||x||,,s!   = AA0B BBc                0   t        j                          }t        | j                        t        kD  rB|t        z
  }| j                  j                         D ci c]  \  }}||kD  s|| c}}| _        || j                  v ry|| j                  |<   yc c}}w )NTF)r   r(  r   r<   r;   items)r$   rr  rm  cutoffkeytss         r&   r  zQQAdapter._is_duplicate  s    iikt""#n4//F'+':':'@'@'B##Cb6kR#D T(((&)F##s   B%B)returnr    )r!   r    r  r  )r  r  r  rx   )r   r   )r   rx   r  rx   )r   r    r  r  )r   r   r  rx   )r6  Dict[str, Any]r  r  )r:  r   r  r  )rk  r   r  Optional[Dict[str, Any]])rr  r    r  r   )r{  r   r  r  )r  r    r:  r   r  r  )r  z7Optional[Callable[[InteractionEvent], Awaitable[None]]]r  r  )r   )r  r    r   r   r  r  )r  r    r  Optional[Dict[str, str]])r{  rL   r  r    r  rx   )r{  rL   r  r  r(   )r  r    r  r    r  r  )r:  r  rr  r    r  r    r  r  r~  r    r  r  )r:  r  r  r  )r  r    r  r    r  r    )r  r  r  r  )r  r   r  r  )r   r    r  r    r<  r    r  Optional[str])r  r    r  r    r  rx   )r  zDict[str, str])r   r    r  r    r  r    r   r  r!  r  r  r  )rd  bytesr  r    r  r  )r   r  r  r    )r   r  r  rx   )ro  r    re  r    r  r  )rd  r  re  r    r  r  )r  r  )re  r    r  r  )rd  r  r  r    r  r  )
r  r    r)  r    r  r  r   floatr  r  )NNFN)r  r    r  r    r  r   r   r  r  r  r  rx   r  r  r  r  )NN)
ry  r    r  r    r  r  r  r  r  r   rH  )ry  r    r  r    r  r  r  r   )
r  r    r  r    r  r  r  Optional[InlineKeyboard]r  r   )
r  r    r  r    r  r  r  r  r  r   )r  r    r  r    r  r  r  r   )
ry  r    r  r    r  rK   r  r  r  r   )ry  r    r  rJ   r  r  r  r   )zdangerous commandNTF)ry  r    r   r    r  r    r  r    r  r  r  rx   r!  rx   r  r   )r   r   N)ry  r    r$  r    r%  r    r  r    r  r  r  r   )r  r    r  r  r  r  )NNN)ry  r    r0  r    r1  r  r  r  r  r  r  r   )
ry  r    r5  r    r1  r  r  r  r  r   )
ry  r    r;  r    r1  r  r  r  r  r   )
ry  r    r>  r    r1  r  r  r  r  r   )ry  r    rA  r    r1  r  r  r  r  r  r  r   )ry  r    rI  r    r  r   rJ  r    r1  r  r  r  r  r  r  r   )r  r    ry  r    rI  r    r  r   r  r  r  zTuple[str, Dict[str, Any]])rx  r    r  r  r  zTuple[str, str, str])ry  r    r  r  )r  r    r  r    )ry  r    r  r  )rx  r    r  rx   )ry  r    r  r    )r  r    r  rx   )r  r    r  r    r  rx   )r  	List[str]r  r    r  rx   )rk  r    r  r   )rr  r    r  rx   )hr)   r*   r+   r,   SUPPORTS_MESSAGE_EDITINGr:   rl  rk  propertyrd   rm   rv   r|   r#   r   r   r   r   r   r   r   r   r   r  r  r   rD  rJ  staticmethodrO  r0  rb  r/  ru  rz  rc  r  rd  r  r  r  r  r   r  r  r  r  r  r  r  r  r  r(  r&  r8  r'  rY  ri  rz  rk  rm  rl  r  r[  r;  r2   r  r  r  r  r  r  r  r  r   r  r  r  r"  r  r'  r
  r3  r7  r<  r?  rB  r.  rF  rf  ro  r  rr  r/  r  r  r~  r  r  r  r  r  r  r-   r.   s   @r&   r\   r\      s
   T  %+! (
" H H>HH     5: ?B82(0!&F4R>p%d.72&#QJ"B 
 
O?bV > > * *,Q>.I. 
.7x   
	J  "  #  
	2I
#I
 
I
V E E,])]) ]) 	])
 #]) ]) 
])~?)?) ?) 	?)
 #?) ?) 
?)BI)I) I) 	I)
 #I) I) 
I)VD)D) D) 	D)
 #D) D) 
D)T\
\
 
\
|      &o
o
 
o
d EG+=+=*-+=>A+=	+=Z  *	" -1+/zz z 	z *z )z 
zx0#0/20	0d  & Y Y4l&+Z6p1f;#;14;	;J .20"O"O "O +	"O
 "O 
"OR "&'+!&'+,, , 	,
 , %, , %, 
,^ #"4 '+15  $	
 / 
J '+	0O0O 0O $	0O
 
0Ol '+15NN N $	N
 /N 
N4 '+15NN N $	N
 /N 
N2 LP
N!
N,/
N;H
N	
N* '+/=/= /= %	/=
 $/= 
/=j '+	

 !
 $	

 

P  315 $"$
$
 $
 	$

 $
 /$
 $
 $
 
$
L !$ !15

 
 	

 
 /
 

> ;?*7	D &*&*15UU U #	U
 $U /U 
U< &*&*

 
 #	

 $
 

$ &*&*

 
 #	

 $
 

$ &*&*

 
 #	

 $
 

$ &*'+&*

 
 #	

 %
 $
 

6 &*&*'+|=|= |= 	|=
 |= #|= $|= %|= 
|=|.'.' .' 	.'
 .' %.' 
$.'b ;?$0$0*7$0	$0T&LX'
 A A  S
	  -$
r'   r\   r  )rY   r   r  r  )[r,   
__future__r   r   rc  r   loggingr9  r   r   ro  r   r   pathlibr   typingr   r   r	   r
   r   r   r   urllib.parser   r   rS   r  r   rT   gateway.configr   r   gateway.platforms.baser   r   r   r   r   r   r   gateway.platforms.helpersr   	getLoggerr)   r   r   r   !gateway.platforms.qqbot.constantsr/   r0   r1   r2   r3   r4   r5   r6   r7   r8   r9   r:   r;   r<   r=   r>   r?   r@   rA   rB   rC   rD   gateway.platforms.qqbot.utilsrE   rX   rF   &gateway.platforms.qqbot.chunked_uploadrG   rH   rI   r  rJ   rK   rL   rM   rN   rO   rP   rQ   rV   rZ   r\   rU   r'   r&   <module>r     s  > #      	   '  H H H !
O
 4   5			8	$	W9 	W      0 
	 	 	1
$K0# K0O  G  OEs$   D D 	DD	DD