
    i7                      d Z ddlmZ ddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlmZmZ ddlmZ ddlmZmZmZmZmZ ddlmZ 	 ddlZdZn# e$ r d	ZdZY nw xY w	 ddlZdZn# e$ r d	ZdZY nw xY wdd
lmZmZ ddlm Z m!Z!m"Z"m#Z#m$Z$m%Z% ddl&m'Z'  ej(        e)          Z* G d de+          Z,dZ-dZ.dZ/dZ0dZ1dZ2g dZ3dZ4dZ5dZ6dZ7dZ8dZ9dZ:dZ;dZ<dZ=dZ>d Z?dZ@dZAd!ZBd+d$ZCd,d(ZD G d) d*e           ZEdS )-uY  
QQ Bot platform adapter using the Official QQ Bot API (v2).

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

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

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

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

    Carries the close code and reason for proper handling in the reconnect loop.
     c                    |rt          |          nd | _        |rt          |          nd| _        t	                                          d| j         d| j         d           d S )Nr   zWebSocket closed (code=z	, reason=))intcodestrreasonsuper__init__)selfr   r   	__class__s      ?/home/agentuser/.hermes/hermes-agent/gateway/platforms/qqbot.pyr    zQQCloseError.__init__Q   se    !%/CIII4	%+3c&kkkU49UUt{UUUVVVVV    )r   )__name__
__module____qualname____doc__r    __classcell__r"   s   @r#   r   r   K   sQ         
W W W W W W W W W Wr$   r   zhttps://api.sgroup.qq.comz)https://bots.qq.com/app/getAppAccessTokenz/gateway      >@g      ^@g      4@)      
      <   d   r0   g      @   i  i,    r,               returnboolc                     t           ot          S )z/Check if QQ runtime dependencies are available.)AIOHTTP_AVAILABLEHTTPX_AVAILABLE r$   r#   check_qq_requirementsr>   x   s    00r$   valuer   	List[str]c                X   | g S t          | t                    rd |                     d          D             S t          | t          t          t
          f          rd | D             S t          |                                           r"t          |                                           gng S )z0Coerce config values into a trimmed string list.Nc                ^    g | ]*}|                                 |                                 +S r=   )strip.0items     r#   
<listcomp>z _coerce_list.<locals>.<listcomp>   s-    JJJTZZ\\J

JJJr$   ,c                    g | ]D}t          |                                          #t          |                                          ES r=   )r   rC   rD   s     r#   rG   z _coerce_list.<locals>.<listcomp>   s=    IIIds4yy7H7HID		!!IIIr$   )
isinstancer   splitlisttuplesetrC   )r?   s    r#   _coerce_listrO   }   s    }	% KJJS)9)9JJJJ%$s+,, JIIeIIII#&u::#3#3#5#5=CJJ2=r$   c                  r    e Zd ZdZdZddZeZd fd
Zedd            Z	ddZ
ddZddZddZddZddZddZddZddZddZddZddZed             ZddZdd"Zedd%            Zedd'            Zdd)Zdd-Zdd.Zdd/Zdd0Z edd4            Z!dd6Z"dd:Z#edd<            Z$dd>Z%d?d?d@ddCZ&ddFZ'eddH            Z(eddI            Z)eddL            Z*eddM            Z+eddN            Z,ddPZ-ddQZ.ddSZ/d?e0fddYZ1	 	 	 	 ddd`Z2	 	 dddeZ3	 dddfZ4	 dddhZ5	 dddjZ6	 dddlZ7dddmZ8	 	 	 dddpZ9	 	 dddrZ:	 	 dddtZ;	 	 dddvZ<	 	 	 dddxZ=	 	 	 ddd{Z>	 ddd~Z?dddZ@ddZAddZBedd            ZCddZDedd            ZEddZFddZGedd            ZHddZIddZJ xZKS )	QQAdapterzJQQ Bot adapter backed by the official QQ Bot WebSocket Gateway + REST API.Fr   r   r8   Nonec                    | j                                         D ]8}|                                s"|                    t	          |                     9| j                                          dS )z"Fail all pending response futures.N)_pending_responsesvaluesdoneset_exceptionRuntimeErrorclear)r!   r   futs      r#   _fail_pendingzQQAdapter._fail_pending   sh    *1133 	8 	8C88:: 8!!,v"6"6777%%'''''r$   configr   c                   t                                          |t          j                   |j        pi }t          |                    d          pt          j        dd                    	                                | _
        t          |                    d          pt          j        dd                    	                                | _        t          |                    dd                    | _        t          |                    dd	                    	                                                                | _        t!          |                    d
          p|                    d                    | _        t          |                    dd	                    	                                                                | _        t!          |                    d          p|                    d                    | _        d | _        d | _        d | _        d | _        d | _        d| _        d | _        d | _        i | _        i | _        i | _        d | _        d| _         tC          j"                    | _#        i | _$        d S )Napp_id	QQ_APP_IDr   client_secretQQ_CLIENT_SECRETmarkdown_supportT	dm_policyopen
allow_from	allowFromgroup_policygroup_allow_fromgroupAllowFromr+           )%r   r    r   QQBOTextrar   getosgetenvrC   _app_id_client_secretr9   _markdown_supportlower
_dm_policyrO   _allow_from_group_policy_group_allow_from_session_ws_http_client_listen_task_heartbeat_task_heartbeat_interval_session_id	_last_seq_chat_type_maprT   _seen_messages_access_token_token_expires_atasyncioLock_token_lock_upload_cache)r!   r\   rl   r"   s      r#   r    zQQAdapter.__init__   s   000"599X..L")K2L2LMMSSUU!%))O"<"<"a	J\^`@a@abbhhjj!%eii0BD&I&I!J!J eiiV<<==CCEEKKMM'		,(?(?(Y599[CYCYZZ >6!B!BCCIIKKQQSS!-eii8J.K.K.juyyYiOjOj!k!k :>>B9=487;*. *.(,.0 >@02 -1(+"<>> 9;r$   c                    dS )NQQBotr=   r!   s    r#   namezQQAdapter.name   s    wr$   r9   c                $  K   t           s=d}|                     d|d           t                              d| j        |           dS t
          s=d}|                     d|d           t                              d| j        |           dS | j        r| j        s=d	}|                     d
|d           t                              d| j        |           dS |                     d| j        d          sdS 	 t          j
        dd          | _        |                                  d{V  |                                  d{V }t                              d| j        |           |                     |           d{V  t!          j        |                                           | _        t!          j        |                                           | _        |                                  t                              d| j                   dS # t.          $ ry}d| }|                     d|d           t                              d| j        |d           |                                  d{V  |                                  Y d}~dS d}~ww xY w)z9Authenticate, obtain gateway URL, and open the WebSocket.z(QQ startup failed: aiohttp not installedqq_missing_dependencyT	retryablez![%s] %s. Run: pip install aiohttpFz&QQ startup failed: httpx not installedz[%s] %s. Run: pip install httpxz>QQ startup failed: QQ_APP_ID and QQ_CLIENT_SECRET are requiredqq_missing_credentialsz[%s] %szqqbot-appidzQQBot app IDr+   )timeoutfollow_redirectsNz[%s] Gateway URL: %sz[%s] ConnectedzQQ startup failed: qq_connect_error)exc_info)r;   _set_fatal_errorloggerwarningr   r<   rp   rq   _acquire_platform_lockhttpxAsyncClientrz   _ensure_token_get_gateway_urlinfo_open_wsr   create_task_listen_loopr{   _heartbeat_loopr|   _mark_connected	Exceptionerror_cleanup_release_platform_lock)r!   messagegateway_urlexcs       r#   connectzQQAdapter.connect   s       	@G!!"97d!SSSNN>	7SSS5 	>G!!"97d!SSSNN<diQQQ5| 	4#6 	VG!!":Gt!TTTNN9di9995 **4<
 
 	 5	 % 1$QU V V VD $$&&&&&&&&& !% 5 5 7 7777777KKK.	;GGG --,,,,,,,,, !( 3D4E4E4G4G H HD#*#6t7K7K7M7M#N#ND   """KK($)4444 	 	 	1C11G!!"4g!NNNLLDIwLFFF--//!!!!!!!'')))55555	s   5DH 
JA.J

Jc                  K   d| _         |                                  | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        | j        rD| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        |                                  d{V  |                                  t          
                    d| j                   dS )z)Close all connections and stop listeners.FNz[%s] Disconnected)_running_mark_disconnectedr{   cancelr   CancelledErrorr|   r   r   r   r   r   r   s    r#   
disconnectzQQAdapter.disconnect   sJ     !!! 	%$$&&&''''''''')    $D 	( '')))*********)   #'D mmoo##%%%'33333s#   A AA
B B*)B*c                  K   | j         r+| j         j        s| j                                          d{V  d| _         | j        r+| j        j        s| j                                         d{V  d| _        | j        r&| j                                         d{V  d| _        | j                                        D ]8}|                                s"|	                    t          d                     9| j                                         dS )z*Close WebSocket, HTTP session, and client.NDisconnected)ry   closedcloserx   rz   acloserT   rU   rV   rW   rX   rY   )r!   rZ   s     r#   r   zQQAdapter._cleanup  s.     8 	#DHO 	#(.."""""""""= 	(!5 	(-%%''''''''' 	%#**,,,,,,,,, $D *1133 	@ 	@C88:: @!!,~">">???%%'''''r$   c                  K   | j         r&t          j                    | j        dz
  k     r| j         S | j        4 d{V  | j         r8t          j                    | j        dz
  k     r| j         cddd          d{V  S 	 | j                            t          | j        | j        dt                     d{V }|
                                 |                                }n%# t          $ r}t          d|           |d}~ww xY w|                    d          }|st          d|           t          |                    dd	                    }|| _         t          j                    |z   | _        t                               d
| j        |           | j         cddd          d{V  S # 1 d{V swxY w Y   dS )zFReturn a valid access token, refreshing if needed (with singleflight).r0   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   rz   post	TOKEN_URLrp   rq   DEFAULT_API_TIMEOUTraise_for_statusr   r   rX   rm   r   r   r   r   )r!   respdatar   tokenr   s         r#   r   zQQAdapter._ensure_token0  s      	&$)++0F0K"K"K%%# 	& 	& 	& 	& 	& 	& 	& 	&! *dikkD4JR4O&O&O)	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&
	Y!.33#'<ATUU/ 4        
 %%'''yy{{ Y Y Y"#N#N#NOOUXXY HH^,,E Z"#XRV#X#XYYYTXXlD99::J!&D%)Y[[:%=D"KKEtyR\]]]%1	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	& 	&s7   -F.>A"C! F.!
D+C>>DBF..
F8;F8c                  K   |                                   d{V }	 | j                            t           t           dd| it
                     d{V }|                                 |                                }n%# t          $ r}t          d|           |d}~ww xY w|                    d          }|st          d|           |S )z2Fetch the WebSocket gateway URL from the REST API.NAuthorizationQQBot )headersr   z"Failed to get QQ Bot gateway URL: urlz%QQ Bot gateway response missing url: )
r   rz   rm   API_BASEGATEWAY_URL_PATHr   r   r   r   rX   )r!   r   r   r   r   r   s         r#   r   zQQAdapter._get_gateway_urlO  s#     ((********		T*../-//(*:5*:*:;+ /        D
 !!###99;;DD 	T 	T 	TICIIJJPSS	T hhuoo 	OMtMMNNN
s   A#B 
B$BB$r   c                  K   | j         r+| j         j        s| j                                          d{V  d| _         | j        r+| j        j        s| j                                         d{V  d| _        t	          j                    | _        | j                            |t                     d{V | _         t          	                    d| j
        |           dS )z2Open a WebSocket connection to the QQ Bot gateway.Nr   z[%s] WebSocket connected to %s)ry   r   r   rx   aiohttpClientSession
ws_connectCONNECT_TIMEOUT_SECONDSr   r   r   )r!   r   s     r#   r   zQQAdapter._open_wsf  s       8 	#DHO 	#(.."""""""""= 	(!5 	(-%%'''''''''-//11+ 2 
 
 
 
 
 
 
 
 	4diMMMMMr$   c                :  K   d}d}d}| j         r	 t          j                    }|                                  d{V  d}d}nL# t          j        $ r Y dS t          $ rn}| j         sY d}~dS |j        }t          	                    d| j
        ||j                   t          j                    |z
  }|t          k     rw|dk    rq|dz  }t                              d| j
        ||           |t          k    r>t                              d| j
                   |                     dd	d
           Y d}~dS nd}|                                  |                     d           |dv rO|dk    rdnd}t                              d| j
        |           |                     d| d| d           Y d}~dS |dk    rt                              d| j
        t&                     |t(          k    rY d}~dS t	          j        t&                     d{V  |                     |           d{V rd}d}n|dz  }Y d}~0|dk    r.t                              d| j
                   d| _        d| _        |dv r/t                              d| j
        |           d| _        d| _        |                     |           d{V rd}d}n|dz  }Y d}~nd}~wt6          $ r}| j         sY d}~dS t          	                    d| j
        |           |                                  |                     d           |t(          k    r&t                              d| j
                   Y d}~dS |                     |           d{V rd}d}n|dz  }Y d}~nd}~ww xY w| j         dS dS )u  Read WebSocket events and reconnect on errors.

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

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

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

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

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

        This avoids ``RuntimeError: no running event loop`` when tests call
        ``_dispatch_payload`` synchronously outside of ``asyncio.run()``.
        N)r   get_running_loopr   rX   )coroloops     r#   _create_taskzQQAdapter._create_taskU  sK    	+--D##D))) 	 	 	44	s   '* 
88r   Dict[str, Any]c                "   |                     d          }|                     d          }|                     d          }|                     d          }t          |t                    r| j        || j        k    r|| _        |dk    rt          |t                    r|ni }|                     dd          }|d	z  d
z  | _        t                              d| j        || j                   | j	        r/| j        (| 
                    |                                            n'| 
                    |                                            dS |dk    r|r|dk    r|                     |           nu|dk    r!t                              d| j                   nN|dv r)t          j        |                     ||                     n!t                              d| j        |           dS |dk    rdS t                              d| j        |           dS )zPRoute inbound WebSocket payloads (dispatch synchronously, spawn async handlers).r   tsr   Nr.   heartbeat_intervali0u  g     @@g?zB[%s] Hello received, heartbeat_interval=%dms (sending every %.1fs)r   READYRESUMEDz[%s] Session resumed)C2C_MESSAGE_CREATEGROUP_AT_MESSAGE_CREATEDIRECT_MESSAGE_CREATEGUILD_MESSAGE_CREATEGUILD_AT_MESSAGE_CREATEz[%s] Unhandled dispatch: %s   z[%s] Unknown op: %s)rm   rJ   r   r   dictr}   r   r   r   r~   r  r  r  _handle_readyr   r   r   _on_message)r!   r   r   r  r  r   d_datainterval_mss           r#   r   zQQAdapter._dispatch_payloadb  s   [[KKKKKKa 	4>#9Q=O=ODN 88$Q--5QQ2F **%95AAK'2V';c'AD$LL]	;0HJ J J  9DN$>!!$"3"3"5"56666!!$"5"5"7"7888F 77q7G||""1%%%%i2DI>>>> 3 3 3 #D$4$4Q$:$:;;;;:DIqIIIF 88F*DIr:::::r$   r   r   c                    t          |t                    rB|                    d          | _        t                              d| j        | j                   dS dS )u7   Handle the READY event — store session_id for resume.r  z[%s] Ready, session_id=%sN)rJ   r  rm   r~   r   r   r   )r!   r   s     r#   r  zQQAdapter._handle_ready  sW    a 	R uu\22DKK3TY@PQQQQQ	R 	Rr$   rawOptional[Dict[str, Any]]c                    	 t          j        |           }n-# t          $ r  t                              dd|            Y d S w xY wt          |t                    r|nd S )Nz[%s] Failed to parse JSON: %rr   )r   loadsr   r   r   rJ   r  )r  r   s     r#   r   zQQAdapter._parse_json  sj    	jooGG 	 	 	LL8'3GGG44	 %Wd33=ww=s    &A Amsg_idc                    t          t          j                              dz  }t          t          j                    j        dd         d          }||z  dz  S )z5Generate a message sequence number in 0..65535 range.i Nr7      i   )r   r   uuiduuid4hex)r#  	time_partrands      r#   _next_msg_seqzQQAdapter._next_msg_seq  sM     	$$y0	4:<<#BQB',,D E))r$   
event_typec                P  K   t          |t                    sdS t          |                    dd                    }|r|                     |          r#t
                              d| j        |           dS t          |                    dd                    }t          |                    dd                                                    }t          |                    d          t                    r|                    d          ni }|dk    r!| 	                    |||||           d{V  dS |d	v r!| 
                    |||||           d{V  dS |d
v r!|                     |||||           d{V  dS |dk    r!|                     |||||           d{V  dS dS )z(Process an inbound QQ Bot message event.Nidr   z([%s] Duplicate or missing message id: %s	timestampcontentauthorr  )r  )r  r  r  )rJ   r  r   rm   _is_duplicater   r   r   rC   _handle_c2c_message_handle_group_message_handle_guild_message_handle_dm_message)r!   r,  r   r#  r/  r0  r1  s          r#   r  zQQAdapter._on_message  s     !T"" 	F QUU4__%% 	++F33 	LLCTYPVWWWFk2..//	aeeIr**++1133$.quuX$E$EMx2 ---**1fgvyQQQQQQQQQQQ777,,QSSSSSSSSSSSNNN,,QSSSSSSSSSSS222))!VWfiPPPPPPPPPPP 32r$   r0  r1  r/  c                  K   t          |                    dd                    }|sdS |                     |          sdS |}|                    d          }t                              d||r
|dd         nd|r)t          |t                    rt          |          nd dnd	           |rt          |t                    rt          |          D ]\  }	}
t          |
t                    rot                              d
|	|
                    dd          t          |
                    dd                    dd         |
                    dd                     | 
                    |           d{V }|d         }|d         }|d         }|d         }|rEd                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}t                              dt          |          t          |                     |                                s|sdS d| j        |<   t          |                     ||d          ||                     ||          |||||                     |                    }|                     |           d{V  dS )z%Handle a C2C (private) message event.user_openidr   Nattachmentsz1[QQ] C2C message: id=%s content=%r attachments=%s2   r   z itemsrR   z9[QQ]   attachment[%d]: content_type=%s url=%s filename=%scontent_typer   P   filename
image_urlsimage_media_typesvoice_transcriptsattachment_info


z*[QQ] After processing: images=%d, voice=%dc2cdmchat_iduser_id	chat_typesourcetextmessage_typeraw_message
message_id
media_urlsmedia_typesr/  )r   rm   _is_dm_allowedr   r   rJ   rL   r   	enumerater  _process_attachmentsjoinrC   r   r   build_source_detect_message_type_parse_qq_timestamphandle_message)r!   r   r#  r0  r1  r/  r8  rL  attachments_raw_i_att
att_resultr>  r?  r@  rA  voice_blockevents                     r#   r3  zQQAdapter._handle_c2c_message  s2      &**]B7788 	F"";// 	F%%..GG;GCRCLL&3z/4/P/PWs?+++VW____,2	4 	4 	4  	:z/4@@ 	:%o66 : :DdD)) :KK [ "DHH^R$@$@ #DHHUB$7$7 8 8" = $R 8 8: : :  44_EEEEEEEE
-
&':;&':;$%67  	Z))$566K<@JJLLYD6MK/66888kD 	b@D

aD6MO3::<<<RaD@
OOS):%;%;	= 	= 	= zz|| 	J 	F+0K($$## %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r$   c                  K   t          |                    dd                    }|sdS |                     |t          |                    dd                              sdS |                     |          }|                     |                    d                     d{V }|d         }	|d         }
|d         }|d	         }|rEd
                    |          }|                                r|dz   |z                                   n|}|r0|                                r|dz   |z                                   n|}|                                s|	sdS d| j        |<   t          | 	                    |t          |                    dd                    d          || 
                    |	|
          |||	|
|                     |                    }|                     |           d{V  dS )zHandle a group @-message event.group_openidr   Nmember_openidr9  r>  r?  r@  rA  rB  rC  grouprF  rJ  )r   rm   _is_group_allowed_strip_at_mentionrT  rU  rC   r   r   rV  rW  rX  rY  )r!   r   r#  r0  r1  r/  ra  rL  r]  r>  r?  r@  rA  r^  r_  s                  r#   r4  zQQAdapter._handle_group_message  s.      1554455 	F%%lC

?TV8W8W4X4XYY 	F %%g..44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67  	Z))$566K<@JJLLYD6MK/66888kD 	b@D

aD6MO3::<<<RaDzz|| 	J 	F,3L)$$$FJJ;;<<! %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r$   c                ^  K   t          |                    dd                    }|sdS t          |                    d          t                    r|                    d          ni }t          |                    dd                    p"t          |                    dd                    }|}	|                     |                    d                     d{V }
|
d         }|
d	         }|
d
         }|
d         }|rEd                    |          }|	                                r|	dz   |z                                   n|}	|r0|	                                r|	dz   |z                                   n|}	|	                                s|sdS d| j        |<   t          | 	                    |t          |                    dd                    |pdd          |	| 
                    ||          |||||                     |                    }|                     |           d{V  dS )z%Handle a guild/channel message event.
channel_idr   Nmembernickusernamer9  r>  r?  r@  rA  rB  rC  guildr.  rc  )rG  rH  	user_namerI  rJ  )r   rm   rJ   r  rT  rU  rC   r   r   rV  rW  rX  rY  )r!   r   r#  r0  r1  r/  rg  rh  ri  rL  r]  r>  r?  r@  rA  r^  r_  s                    r#   r5  zQQAdapter._handle_guild_message1  sQ      |R0011
 	F$.quuX$E$EMx26::fb))**Mc&**Z2L2L.M.M44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67 	Z))$566K<@JJLLYD6MK/66888kD 	b@D

aD6MO3::<<<RaDzz|| 	J 	F*1J'$$"FJJtR0011,$!	 %   22:?PQQ!)..y99
 
 
 !!%(((((((((((r$   c                N  K   t          |                    dd                    }|sdS |}|                     |                    d                     d{V }|d         }	|d         }
|d         }|d         }|rEd	                    |          }|                                r|d
z   |z                                   n|}|r0|                                r|d
z   |z                                   n|}|                                s|	sdS d| j        |<   t          |                     |t          |                    dd                    d          ||                     |	|
          |||	|
| 	                    |                    }| 
                    |           d{V  dS )z Handle a guild DM message event.guild_idr   Nr9  r>  r?  r@  rA  rB  rC  rE  r.  rF  rJ  )r   rm   rT  rU  rC   r   r   rV  rW  rX  rY  )r!   r   r#  r0  r1  r/  rn  rL  r]  r>  r?  r@  rA  r^  r_  s                  r#   r6  zQQAdapter._handle_dm_message^  s      quuZ,,-- 	F44QUU=5I5IJJJJJJJJ
-
&':;&':;$%67 	Z))$566K<@JJLLYD6MK/66888kD 	b@D

aD6MO3::<<<RaDzz|| 	J 	F(,H%$$ FJJtR0011 %  
 22:?PQQ!)..y99
 
 
 !!%(((((((((((r$   rP  rL   rQ  c                <   | st           j        S |st           j        S |r|d                                         nd}d|v sd|v sd|v rt           j        S d|v rt           j        S d|v sd|v rt           j        S t                              d	|           t           j        S )
z4Determine MessageType from attachment content types.r   r   audiovoicesilkvideoimagephotoz8[QQ] Unknown media content_type '%s', defaulting to TEXT)r   r   PHOTOrs   VOICEVIDEOr   r   )rP  rQ  
first_types      r#   rW  zQQAdapter._detect_message_type  s      	$## 	%$$/:B[^))+++
j  Gz$9$9Vz=Q=Q$$j  $$j  Gz$9$9$$ 	OQ[\\\r$   r9  c                  K   t          |t                    sg g g ddS g }g }g }g }|D ]}t          |t                    st          |                    dd                                                                                    }t          |                    dd                                                    }t          |                    dd                    }	|                    d          rd| }
n|r|}
nd}
t          	                    d||
d	d
         |	           | 
                    ||	          rSt          |                    d          t                    r5t          |                    dd                                                    nd}t          |                    d          t                    r5t          |                    dd                                                    nd}|                     |
||	|pd	|pd	           d	{V }|r5|                    d|            t                              d|           0t                              d|
d	d                    |                    d           j|                    d          r	 |                     |
|           d	{V }|rLt           j                            |          r-|                    |           |                    |pd           n|rt                              d|           	# t&          $ r&}t          	                    d|           Y d	}~4d	}~ww xY w	 |                     |
|           d	{V }|r|                    d|	p| d           x# t&          $ r&}t          	                    d|           Y d	}~d	}~ww xY w|rd                    |          nd}||||dS )u  Process inbound attachments (all message types).

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

        Returns a dict with:
        - image_urls: list[str]  — cached local image paths
        - image_media_types: list[str] — MIME types of cached images
        - voice_transcripts: list[str] — STT transcripts for voice messages
        - attachment_info: str — text description of non-image, non-voice attachments
        r   )r>  r?  r@  rA  r;  r   r=  //https:z@[QQ] Processing attachment: content_type=%s, url=%s, filename=%sNr<  asr_refer_textvoice_wav_urlr}  r~  z[Voice] z[QQ] Voice transcript: %sz[QQ] Voice STT failed for %sr0   u   [Voice] [语音识别失败]image/z
image/jpegz)[QQ] Cached image path does not exist: %sz[QQ] Failed to cache image: %sz[Attachment: ]z#[QQ] Failed to cache attachment: %srB  )rJ   rL   r  r   rm   rC   rs   
startswithr   r   _is_voice_content_type_stt_voice_attachmentappendr   r   _download_and_cachern   pathisfiler   rU  )r!   r9  r>  r?  r@  other_attachmentsattcturl_rawr=  r   	asr_referr~  
transcriptcached_pathr   rA  s                    r#   rT  zQQAdapter._process_attachments  s      +t,, 	D"$2)+D D D !#
')')') :	M :	MCc4(( SWW^R00117799??AAB#''%,,--3355G377:r2233H!!$'' (w(( LL[S"Xx1 1 1 **2x88 (M "#''*:";";SAAJC 0"5566<<>>>GI  "#''/":":C@@IC4455;;===FH 
 $(#=#=X#,#4"/"74 $> $ $      

  M%,,-D
-D-DEEEKK ;ZHHHHNN#A3ss8LLL%,,-KLLLLx(( MH(,(@(@b(I(I"I"I"I"I"I"IK" arw~~k'B'B a"))+666)001C|DDDD$ a'RT_```  H H HLL!A3GGGGGGGGHM(,(@(@b(I(I"I"I"I"I"I"IK" T)001RR1R1R1RSSS  M M MLL!FLLLLLLLLM ;LS$))$5666QS$!2!2.	
 
 	
s1   )BL22
M"<MM"&9N!!
O+OOr   r;  Optional[str]c                  K   ddl m}  ||          st          d|dd                    | j        sdS 	 | j                            |d|                                            d{V }|                                 |j        }nB# t          $ r5}t          
                    d| j        |dd         |           Y d}~dS d}~ww xY w|                    d	          r&t          j        |          pd
}t          ||          S |dk    s|                    d          r|                     ||           d{V S t#          t%          |          j                  j        pd}t)          ||          S )z$Download a URL and cache it locally.r   )is_safe_urlzBlocked unsafe URL: Nr<  r+   )r   r   z[%s] Download failed for %s: %sr  z.jpgrq  audio/qq_attachment)tools.url_safetyr  
ValueErrorrz   rm   _qq_media_headersr   r0  r   r   r   r   r  	mimetypesguess_extensionr   _convert_audio_to_wavr   r   r  r   )	r!   r   r;  r  r   r   r   extr=  s	            r#   r  zQQAdapter._download_and_cache  s     000000{3 	@>CH>>???  	4	*..T4+A+A+C+C /        D !!###<DD 	 	 	LL:DIs3B3xQTUUU44444	 ""8,, 		=+L99CVC)$444W$$(?(?(I(I$ 33D#>>>>>>>>>HSMM.//4GH,T8<<<s   AB	 	
C*CCr=  c                   |                                                                  }|                                                                 |dk    s|                    d          rdS d}t          fd|D                       rdS dS )z0Check if an attachment is a voice/audio message.rq  r  T)	.silk.amr.mp3.wav.ogg.m4a.aacz.speex.flacc              3  B   K   | ]}                     |          V  d S N)endswith)rE   r  fns     r#   	<genexpr>z3QQAdapter._is_voice_content_type.<locals>.<genexpr>   s/      ==Cr{{3======r$   F)rC   rs   r  any)r;  r=  r  _VOICE_EXTENSIONSr  s       @r#   r  z QQAdapter._is_voice_content_type  s     !!''))^^##%%==BMM(33=4h====+<===== 	4ur$   Dict[str, str]c                ,    | j         rdd| j          iS i S )zReturn Authorization headers for QQ multimedia CDN downloads.

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

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

        Returns the transcript text, or None on failure.
        z%[QQ] STT: using QQ asr_refer_text: %rNr1   Fr{  r|  Tz1[QQ] STT: using voice_wav_url (pre-converted WAV)z[QQ] STT: no HTTP clientz<[QQ] STT: downloading voice from %s (pre_wav=%s, headers=%s)r<  r+   )r   r   r   z.[QQ] STT: downloaded %d bytes, content_type=%szcontent-typeunknownr.   z8[QQ] STT: downloaded data too small (%d bytes), skippingr   r  suffixdeletez5[QQ] STT: using pre-converted WAV directly (%d bytes)z([QQ] STT: converting to wav, filename=%rz.[QQ] STT: ffmpeg conversion produced no outputz[QQ] STT: calling ASR on %sz[QQ] STT success: %rz'[QQ] STT: ASR returned empty transcriptz,[QQ] STT failed for voice attachment: %s: %s)r   r   r  rz   r   r  r9   rm   r   r0  r   r   tempfileNamedTemporaryFilewriter   _convert_audio_to_wav_filer   exists	_call_sttrn   unlinkOSErrorr   HTTPStatusErrorTransportErrorIOErrorr   r%   )r!   r   r;  r=  r}  r~  download_url
is_pre_wavdownload_headersr   
audio_datar  tmpwav_pathr  r   s                   r#   r  zQQAdapter._stt_voice_attachment/  s     &  	"KK?PTQTPTAUVVV!! 
 	M''-- 9 8 8 8(LJKKKLLL4	$ 9:::t#5577KKV$SbS):t<L7M7MO O O*..d4DW[ /        D !!###JKKHJ)9)9.))T)TV V V :##Y[^_i[j[jkkkt   00u0MM (QTIIj)))"xH( ( ( ( ( ( ( ( ( ( ( ( ( ( ( SUXYcUdUdeeeeFQQQ!%!@!@X!V!VVVVVVV  tH~~'<'<'>'>  NN#STTT4 KK5x@@@#~~h77777777J	(####     J2Jtt4DEEEEHIII%u';WE 	 	 	NNI4PS99K]_bccc44444	su   )!L DL L -G
L GL GB L  6L 7K L 
KL KAL !M8?.M33M8r  bytesc                  K   ddl }t          |          j        r&t          |          j                                        n|                     |          }t
                              dt          |          ||dd                    |                    |d          5 }|	                    |           |j
        }ddd           n# 1 swxY w Y   |                    dd          d         d	z   }|                     ||           d{V }|s|                     ||           d{V }|s|                     ||           d{V }	 t          j        |           n# t"          $ r Y nw xY w|S )
a"  Convert audio bytes to a temp .wav file using pilk (SILK) or ffmpeg.

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

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

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

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

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

        Uses the provider configured in ``channels.qqbot.stt`` config,
        falling back to QQ's built-in ``asr_refer_text`` if not configured.
        Returns None if STT is not configured or the call fails.
        z9[QQ] 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   r0  r   rL  z0[QQ] STT API call failed (model=%s, base=%s): %sr:  )r  r   r   rd   rz   r   r   r   r   r   rm   rC   r   r  r  )r!   r  r  r  r  r	  fr   r  r  r0  rL  r   s                r#   r  zQQAdapter._call_sttU  sL      **,, 	NNVWWW4:&)$ 	h%% !.33666,.A.A.AB!DNN$7K#HI!5)  4                       !!###YY[[FjjB//G +!!*..B77;;IrJJ==?? +"==??*::fb))Dzz|| $zz||#4%w/ 	 	 	NNM (3B3-6 6 644444	sD   F AB1%F 1B55F 8B59BF =F G*%GG
source_urlc                \  K   ddl }t          |          j        r8t          t          |          j                  j                                        nd}|r|dvr|                     |          }|                    |d          5 }|                    |           |j	        }ddd           n# 1 swxY w Y   |
                    dd          d         d	z   }	 |d
k    p|                     |          }|r|                     ||           d{V }	n|                     ||           d{V }	|	sbt                              d| j	        |dd         |           t!          |d|           	 t#          j        |           S # t&          $ r Y S w xY wnH# t(          $ r; t!          |d|           cY 	 t#          j        |           S # t&          $ r Y S w xY ww xY w	 	 t#          j        |           n:# t&          $ r Y n.w xY w# 	 t#          j        |           w # t&          $ r Y w w xY wxY w	 t          |                                          }
t#          j        |           t!          |
d          S # t(          $ r,}t                              d| j	        |           Y d}~dS d}~ww xY w)zLConvert audio bytes to .wav using pilk (SILK) or ffmpeg, caching the result.r   Nr   )r  r  r  r  r  r  r  r  Fr  r  r6   r  r  z/[%s] audio conversion failed for %s (format=%s)r0   qq_voicezqq_voice.wavz%[%s] Failed to read converted wav: %s)r  r   r  r   r  rs   r  r  r  r   r  r  r  r  r   r   r   rn   r  r  r   
read_bytesr   )r!   r  r  r  r  r  r  r  is_silkr  wav_datar   s               r#   r  zQQAdapter._convert_audio_to_wav  s      AI@T@T@Yad8J'',--4::<<<_a 	8c!ccc++J77C((E(BB 	$gMM*%%%|H	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ ??3**1-6	WnI(=(=j(I(IG O#888LLLLLLLL#::8XNNNNNNNN OP Iz#2#= = =0=M=M=MNN	(####   O  	K 	K 	K,Z9IC9I9IJJJJ	(####   	K	O	(####   	(####   	H~~0022HIh,X~FFF 	 	 	LL@$)SQQQ44444	s   B00B47B4BF 0F
FFH G3H 5G


GGGH  G5 5
HHH,HH,
H)&H,(H))H,0AI5 5
J+?!J&&J+methodr  bodyr   floatc                  K   | j         st          d          |                                  d{V }d| dd}	 | j                             |t           | |||           d{V }|                                }|j        dk    r1t          d|j         d	| d
|                    d|                     |S # t          j	        $ r}	t          d| d|	           |	d}	~	ww xY w)z5Make an authenticated REST API request to QQ Bot API.u.   HTTP client not initialized — not connected?Nr   zapplication/json)r   zContent-Type)r   r   r   i  zQQ Bot API error [z] z: r   zQQ Bot API timeout [z]: )
rz   rX   r   requestr   r   status_coderm   r   TimeoutException)
r!   r#  r  r$  r   r   r   r   r   r   s
             r#   _api_requestzQQAdapter._api_request  ss        	QOPPP((********-e--.
 

	O*22#T## 3        D 99;;D3&&"3)9 3 3T 3 3xx	4003 3   K% 	O 	O 	ODdDDsDDEE3N	Os   A?B< <C&C!!C&target_type	target_id	file_type	file_datasrv_send_msg	file_namec                  K   |dk    rd| dnd| d}||d}	|r||	d<   n|r||	d<   |t           k    r|r||	d<   d	}
t          d
          D ]}	 |                     d||	t                     d	{V c S # t          $ r]}|}
t          |          t          fddD                       r |dk     r t          j        d|dz   z             d	{V  Y d	}~d	}~ww xY w|
)z"Upload media and return file_info.rD  
/v2/users//files/v2/groups/)r-  r/  r   r.  r0  Nr2   POSTr   c              3      K   | ]}|v V  	d S r  r=   )rE   kwerr_msgs     r#   r  z*QQAdapter._upload_media.<locals>.<genexpr>  s'      __rW}______r$   )400401Invalidr   Timeoutr,   g      ?r6   )	MEDIA_TYPE_FILEranger*  FILE_UPLOAD_TIMEOUTrX   r   r  r   r   )r!   r+  r,  r-  r   r.  r/  r0  r  r$  last_excattemptr   r8  s                @r#   _upload_mediazQQAdapter._upload_media  sz      2=1E1E-I----KjYbKjKjKj #( 
  
  	*DKK 	* )D''I' )D Qxx 		= 		=G=!..vtTK^.___________ = = =c((____/^_____ Q;;!-w{(;<<<<<<<<<= s   #A66
C ACCrG  reply_tometadatar   c                h  K   ~| j         st          dd          S |r|                                st          d          S |                     |          }|                     || j                  }t          dd          }|D ],}|                     |||           d{V }|j        s|c S d}-|S )zSend a text or markdown message to a QQ user or group.

        Applies format_message(), splits long messages via truncate_message(),
        and retries transient failures with exponential backoff.
        FNot connectedsuccessr   T)rH  z	No chunksN)is_connectedr   rC   format_messagetruncate_messageMAX_MESSAGE_LENGTH_send_chunkrH  )	r!   rG  r0  rC  rD  	formattedchunkslast_resultchunks	            r#   sendzQQAdapter.send  s         	De?CCCC 	,gmmoo 	,d++++''00	&&y$2IJJ kBBB 	 	E $ 0 0% J JJJJJJJK& #""""HHr$   c           	       
K   d}|                      |          }t          d          D ]1}	 |dk    r|                     |||           d{V c S |dk    r|                     |||           d{V c S |dk    r|                     |||           d{V c S t          dd|           c S # t          $ r}|}t          |                                          
t          
fd	d
D                       rY d}~ n\|dk     rHdd|z  z  }t                              d| j        |dz   ||           t          j        |           d{V  Y d}~+d}~ww xY w|rt          |          ndt                              d| j                   t          fddD                        }	t          d|	          S )z5Send a single chunk with retry + exponential backoff.Nr2   rD  rc  rk  FzUnknown chat type for rG  c              3      K   | ]}|v V  	d S r  r=   )rE   kerrs     r#   r  z(QQAdapter._send_chunk.<locals>.<genexpr>-  s'      ^^AqCx^^^^^^r$   )invalid	forbidden	not foundzbad requestr,   g      ?z$[%s] send retry %d/3 after %.1fs: %sr6   zUnknown errorz[%s] Send failed: %sc              3  D   K   | ]}|                                 v V  d S r  )rs   )rE   rU  	error_msgs     r#   r  z(QQAdapter._send_chunk.<locals>.<genexpr>8  sN       L L ! !2!22 L L L L L Lr$   )rW  rX  rY  )rH  r   r   )_guess_chat_typer>  _send_c2c_text_send_group_text_send_guild_textr   r   r   rs   r  r   r   r   r   r   r   )r!   rG  r0  rC  r@  rI  rA  r   r   r   rV  r[  s             @@r#   rM  zQQAdapter._send_chunk  sm      )-))'22	Qxx 	/ 	/G/%%!%!4!4Wgx!P!PPPPPPPPPP'))!%!6!6w!R!RRRRRRRRRR'))!%!6!6w!R!RRRRRRRRRR%e;]T[;];]^^^^^^ / / /#hhnn&&^^^^)]^^^^^ EEEEEQ;;1<0ENN#I#'9gk5#G G G!-........./ &.BCMMM?	+TY	BBB L L L L%JL L L L L L	%yINNNNs0   "B4"B49"B4B44
E>>EAEEopenidc                V  K   |                      |p|          }|                     ||          }|r||d<   |                     dd| d|           d{V }t          |                    dt          j                    j        dd                             }t          d||	          S )
z%Send text to a C2C user via REST API.r#  r5  r2  	/messagesNr.     TrH  rO  raw_response	r+  _build_text_bodyr*  r   rm   r&  r'  r(  r   )r!   r`  r0  rC  msg_seqr$  r   r#  s           r#   r]  zQQAdapter._send_c2c_text<  s       $$X%788$$Wh77 	&%DN&&v/MF/M/M/MtTTTTTTTTTXXdDJLL$4SbS$9::;;$6MMMMr$   ra  c                V  K   |                      |p|          }|                     ||          }|r||d<   |                     dd| d|           d{V }t          |                    dt          j                    j        dd                             }t          d||	          S )
z"Send text to a group via REST API.r#  r5  r4  rb  Nr.  rc  Trd  rf  )r!   ra  r0  rC  rh  r$  r   r#  s           r#   r^  zQQAdapter._send_group_textI  s       $$X%=>>$$Wh77 	&%DN&&v/T\/T/T/TVZ[[[[[[[[TXXdDJLL$4SbS$9::;;$6MMMMr$   rg  c                  K   d|d| j                  i}|r||d<   |                     dd| d|           d{V }t          |                    dt	          j                    j        dd                             }t          d	||
          S )z*Send text to a guild channel via REST API.r0  Nr#  r5  z
/channels/rb  r.  rc  Trd  )rL  r*  r   rm   r&  r'  r(  r   )r!   rg  r0  rC  r$  r   r#  s          r#   r_  zQQAdapter._send_guild_textV  s       !*73KD4K3K+LM 	&%DN&&v/QJ/Q/Q/QSWXXXXXXXXTXXdDJLL$4SbS$9::;;$6MMMMr$   c                    |                      |pd          }| j        rd|d| j                 it          |d}n|d| j                 t          |d}|r| j        sd|i|d<   |S )z2Build the message body for C2C/group text sending.defaultr0  N)markdownmsg_typerh  )r0  rn  rh  rO  message_reference)r+  rr   rL  MSG_TYPE_MARKDOWNMSG_TYPE_TEXT)r!   r0  rC  rh  r$  s        r#   rg  zQQAdapter._build_text_bodyb  s    $$X%:;;! 	&0H1H0H(IJ-"$ $DD ##;D$;#;<)" D  	E) E-98,D()r$   	image_urlcaptionc                ,  K   ~|                      ||t          d||           d{V }|j        s|                     |          s|S t                              d| j        |j                   |r| d| n|}|                     |||           d{V S )z-Send an image natively via QQ Bot API upload.rt  Nz0[%s] Image send failed, falling back to text: %srB  )rG  r0  rC  )	_send_mediaMEDIA_TYPE_IMAGErH  _is_urlr   r   r   r   rR  )r!   rG  rr  rs  rC  rD  r  fallbacks           r#   
send_imagezQQAdapter.send_image~  s       ''<LgW^`hiiiiiiii> 	i!8!8 	M 	I49V\Vbccc07Fg,,,,,YYYw8YTTTTTTTTTr$   
image_pathc                R   K   ~|                      ||t          d||           d{V S )z!Send a local image file natively.rt  N)ru  rv  )r!   rG  rz  rs  rC  kwargss         r#   send_image_filezQQAdapter.send_image_file  >       %%gz;KWV]_ghhhhhhhhhr$   
audio_pathc                R   K   ~|                      ||t          d||           d{V S )zSend a voice message natively.rq  N)ru  MEDIA_TYPE_VOICE)r!   rG  r  rs  rC  r|  s         r#   
send_voicezQQAdapter.send_voice  r~  r$   
video_pathc                R   K   ~|                      ||t          d||           d{V S )zSend a video natively.rs  N)ru  MEDIA_TYPE_VIDEO)r!   rG  r  rs  rC  r|  s         r#   
send_videozQQAdapter.send_video  r~  r$   	file_pathc           	     V   K   ~|                      ||t          d|||           d{V S )zSend a file/document natively.r  )r0  N)ru  r=  )r!   rG  r  rs  r0  rC  r|  s          r#   send_documentzQQAdapter.send_document  sW       %%gy/6SZ\d1: & < < < < < < < < 	<r$   media_sourcekindc                &  K   | j         st          dd          S 	 |                     ||           d{V \  }}	}
|                     |          }|dk    rd| dnd| d}|d	k    rt          dd
          S |                     ||||                     |          s|nd|                     |          r|ndd|t          k    r|
nd           d{V }|                    d          }|st          dd|           S |                     |          }t          d|i|d}|r|d| j
                 |d<   |r||d<   |                     d|dk    rd| dnd| d|           d{V }t          dt          |                    dt          j                    j        dd                             |          S # t           $ rI}t"                              d| j        |           t          dt          |                    cY d}~S d}~ww xY w)z*Upload media and send as a native message.FrF  rG  NrD  r2  r3  r4  rk  z,Guild media send not supported via this path)r.  r   r/  r0  	file_infozUpload returned no file_info: )rn  mediarh  r0  r#  r5  rb  Tr.  rc  rd  z[%s] Media send failed: %s)rI  r   _load_mediar\  rB  rw  r=  rm   r+  MSG_TYPE_MEDIArL  r*  r   r&  r'  r(  r   r   r   r   )r!   rG  r  r-  r  rs  rC  r0  r   r;  resolved_namerI  target_pathuploadr  rh  r$  	send_datar   s                      r#   ru  zQQAdapter._send_media  s        	De?CCCC2	=6:6F6F|U^6_6_0_0_0_0_0_0_-D, --g66I:Cu:L:L6w6666Ro`gRoRoRoKG## "%7effff  --7I&*ll<&@&@J$$d$(LL$>$>HLLD"+4+G+G--T .        F 

;//I b!%7`X^7`7`aaaa ((11G*%y1"$ $D
  D")*B4+B*B"CY *!)X"//3<3E3E/W////KkY`KkKkKk       I
 y}}T4:<<3CCRC3HIIJJ&   
  	= 	= 	=LL5ty#FFFe3s88<<<<<<<<<	=s,   AF= :BF= B:F= =
H>HHHrK  Tuple[str, str, str]c                  K   t          |                                          }|st          d          t          |          }|j        dv r>t          j        |          d         pd}|pt          |j                  j	        pd}|||fS t          |          
                                }|                                s(t          j                    |z                                  }|                                r|                                sL|                    d          st#          |          dk     rt          d|          t%          d	|           |                                }|p|j	        }t          j        t          |                    d         pd}t)          j        |                              d
          }|||fS )zSLoad media from URL or local path. Returns (base64_or_url, content_type, filename).zMedia source is requiredhttphttpsr   zapplication/octet-streamr  <r2   z1Invalid media source (looks like a placeholder): zMedia file not found: ascii)r   rC   r  r   schemer  
guess_typer   r  r   
expanduseris_absolutecwdresolver  is_filer  r   r  r   base64	b64encoder  )	r!   rK  r0  parsedr;  r  
local_pathr  b64s	            r#   r  zQQAdapter._load_media  s      V""$$ 	97888&!!=---$/77:X>XL%Jfk):):)?J7M<66 &\\,,..
%%'' 	=(**z1::<<J  "" 	K**<*<*>*> 	K   %% Vq RRR   $$IZ$I$IJJJ##%%!4Z_ +C
OO<<Q?]C]s##**733L-//r$   c                T  K   ~| j         sdS |                     |          }|dk    rdS 	 |                     |          }t          ddd|d}|                     dd| d	|           d{V  dS # t
          $ r,}t                              d
| j        |           Y d}~dS d}~ww xY w)z<Send an input notify to a C2C user (only supported for C2C).NrD  r6   r0   )
input_typeinput_second)rn  input_notifyrh  r5  r2  rb  z[%s] send_typing failed: %s)	rI  r\  r+  MSG_TYPE_INPUT_NOTIFYr*  r   r   r   r   )r!   rG  rD  rI  rh  r$  r   s          r#   send_typingzQQAdapter.send_typing.  s        	F ))'22	F		H((11G1/0" E E" D
 ##F,K,K,K,KTRRRRRRRRRRR 	H 	H 	HLL6	3GGGGGGGGG	Hs   AA1 1
B';!B""B'c                2    | j         r|S t          |          S )zFormat message for QQ.

        When markdown_support is enabled, content is sent as-is (QQ renders it).
        When disabled, strip markdown via shared helper (same as BlueBubbles/SMS).
        )rr   r   )r!   r0  s     r#   rJ  zQQAdapter.format_messageI  s"     ! 	Ng&&&r$   c                F   K   |                      |          }||dv rdnddS )z/Return chat info based on chat type heuristics.)rc  rk  rc  rE  )r   r   )r\  )r!   rG  rI  s      r#   get_chat_infozQQAdapter.get_chat_infoW  s=      ))'22	(,>>>GGD
 
 	
r$   c                H    t          t          |                     j        dv S )Nr  )r   r   r  )rK  s    r#   rw  zQQAdapter._is_urlc  s    F$$+/@@@r$   c                2    || j         v r| j         |         S dS )zDDetermine chat type from stored inbound metadata, fallback to 'c2c'.rD  )r   )r!   rG  s     r#   r\  zQQAdapter._guess_chat_typeg  s#    d)))&w//ur$   c                `    ddl }|                    dd|                                           }|S )z9Strip the @bot mention prefix from group message content.r   Nz^@\S+\s*r   )resubrC   )r0  r  strippeds      r#   re  zQQAdapter._strip_at_mentionm  s/     				66+r7==??;;r$   rH  c                l    | j         dk    rdS | j         dk    r|                     | j        |          S dS NdisabledF	allowlistT)rt   _entry_matchesru   )r!   rH  s     r#   rR  zQQAdapter._is_dm_allowedu  s?    ?j((5?k))&&t'7AAAtr$   group_idc                l    | j         dk    rdS | j         dk    r|                     | j        |          S dS r  )rv   r  rw   )r!   r  rH  s      r#   rd  zQQAdapter._is_group_allowed|  sA    ++5,,&&t'=xHHHtr$   entriesr@   targetc                    t          |                                                                          }| D ]D}t          |                                                                          }|dk    s||k    r dS EdS )N*TF)r   rC   rs   )r  r  normalized_targetentry
normalizeds        r#   r  zQQAdapter._entry_matches  s    KK--//5577 	 	EU))++1133JS  J2C$C$Ctt %Dur$   r   c                j   |st          j        t          j                  S 	 t          j        |          S # t
          t          f$ r Y nw xY w	 t          j        t          |          dz  t          j                  S # t
          t          f$ r Y nw xY wt          j        t          j                  S )zParse QQ API timestamp (ISO 8601 string or integer ms).

        The QQ API changed from integer milliseconds to ISO 8601 strings.
        This handles both formats gracefully.
        )tzr3   )	r   nowr   utcfromisoformatr  	TypeErrorfromtimestampr   )r!   r  s     r#   rX  zQQAdapter._parse_qq_timestamp  s      	1<8<0000	)#...I& 	 	 	D		)#c((T/hlKKKKI& 	 	 	D	|x|,,,,s!   7 A
A/A? ?BBc                    t          j                     }t          | j                  t          k    r4|t          z
  fd| j                                        D             | _        || j        v rdS || j        |<   dS )Nc                (    i | ]\  }}|k    ||S r=   r=   )rE   keytscutoffs      r#   
<dictcomp>z+QQAdapter._is_duplicate.<locals>.<dictcomp>  s+     # # ##Cb6kkRkkkr$   TF)r   r   r   DEDUP_MAX_SIZEDEDUP_WINDOW_SECONDSitems)r!   r#  r  r  s      @r#   r2  zQQAdapter._is_duplicate  s    ikkt"##n44//F# # # #'+':'@'@'B'B# # #D T(((4&)F#ur$   )r   r   r8   rR   )r\   r   )r8   r   r8   r9   )r8   rR   )r   r   r8   rR   )r   r   r8   r9   )r   r  r8   rR   )r   r   r8   rR   )r  r   r8   r   )r#  r   r8   r   )r,  r   r   r   r8   rR   )r   r  r#  r   r0  r   r1  r  r/  r   r8   rR   )rP  rL   rQ  rL   )r9  r   r8   r  )r   r   r;  r   r8   r  )r;  r   r=  r   r8   r9   )r8   r  )r   r   r;  r   r=  r   r}  r  r~  r  r8   r  )r  r  r=  r   r8   r  )r   r  r8   r   )r   r  r8   r9   )r  r   r  r   r8   r  )r  r  r  r   r8   r  )r8   r  )r  r   r8   r  )r  r  r  r   r8   r  )
r#  r   r  r   r$  r   r   r%  r8   r  )NNFN)r+  r   r,  r   r-  r   r   r  r.  r  r/  r9   r0  r  r8   r  )NN)
rG  r   r0  r   rC  r  rD  r   r8   r   r  )rG  r   r0  r   rC  r  r8   r   )r`  r   r0  r   rC  r  r8   r   )ra  r   r0  r   rC  r  r8   r   )rg  r   r0  r   rC  r  r8   r   )r0  r   rC  r  r8   r  )NNN)rG  r   rr  r   rs  r  rC  r  rD  r   r8   r   )
rG  r   rz  r   rs  r  rC  r  r8   r   )
rG  r   r  r   rs  r  rC  r  r8   r   )
rG  r   r  r   rs  r  rC  r  r8   r   )rG  r   r  r   rs  r  r0  r  rC  r  r8   r   )rG  r   r  r   r-  r   r  r   rs  r  rC  r  r0  r  r8   r   )rK  r   r0  r  r8   r  )rG  r   r8   rR   )r0  r   r8   r   )rG  r   r8   r  )rK  r   r8   r9   )rG  r   r8   r   )rH  r   r8   r9   )r  r   rH  r   r8   r9   )r  r@   r  r   r8   r9   )r  r   r8   r   )r#  r   r8   r9   )Lr%   r&   r'   r(   SUPPORTS_MESSAGE_EDITINGr[   rL  r    propertyr   r   r   r   r   r   r   r   r   r   r   r  r  staticmethodr  r   r  r   r+  r  r3  r4  r5  r6  rW  rT  r  r  r  r  r  r  r  r  r  r  r  r  r  r   r*  rB  rR  rM  r]  r^  r_  rg  ry  r}  r  r  r  ru  r  r  rJ  r  rw  r\  re  rR  rd  r  rX  r2  r)   r*   s   @r#   rQ   rQ      sX       TT  %( ( ( ( ,#; #; #; #; #; #;R    X1 1 1 1f4 4 4 42( ( ( (0& & & &>   .N N N N"i% i% i% i%V   $7 7 7 7&   &M M M M@" " " "6 
 
 \
+; +; +; +;ZR R R R > > > \> * * * \*Q Q Q Q2<) <) <) <)|+) +) +) +)Z+) +) +) +)Z') ') ') ')\       \ $Y
 Y
 Y
 Y
v= = = =< 	 	 	 \		 	 	 	" )-'+U U U U U Un% % % %N    \& Y Y Y \Y ( ( ( \(T    \$    \25 5 5 5n) ) ) )V( ( ( (d *.,!O !O !O !O !OP "#'"#'% % % % %V #'-1    B EI"O "O "O "O "OJ DHN N N N N JNN N N N N HL
N 
N 
N 
N 
N    @ "&"&-1U U U U U0 "&"&
i 
i 
i 
i 
i  "&"&
i 
i 
i 
i 
i  "&"&
i 
i 
i 
i 
i  "&#'"&< < < < <( "&"&#'@= @= @= @= @=F 7;"0 "0 "0 "0 "0PH H H H H6' ' ' '
 
 
 
 A A A \A       \          \- - - -$
 
 
 
 
 
 
 
r$   rQ   r  )r?   r   r8   r@   )Fr(   
__future__r   r   r  r   loggingr  rn   r   r&  r   r   pathlibr   typingr   r   r	   r
   r   urllib.parser   r   r;   r  r   r<   gateway.configr   r   gateway.platforms.baser   r   r   r   r   r   gateway.platforms.helpersr   	getLoggerr%   r   r   r   r   r   r   r   r?  r   r   r   r   r   r   rL  r  r  rq  rp  r  r  rv  r  r  r=  r>   rO   rQ   r=   r$   r#   <module>r     s   > # " " " " "         				   ' ' ' ' ' ' ' '       3 3 3 3 3 3 3 3 3 3 3 3 3 3 ! ! ! ! ! !NNN   GGGLLLOO   OEEE 4 3 3 3 3 3 3 3                5 4 4 4 4 4		8	$	$	W 	W 	W 	W 	W9 	W 	W 	W '7	    &&&               1 1 1 1
> > > >\ \ \ \ \# \ \ \ \ \s$   A 	AA#A* *	A65A6