
    `gj1X                       d Z ddlmZ ddlZddlZddlZddlZddlmZ ddl	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dZd	Zd
ZdZdZdZdZdZdZdZ dZ!dZ" G d de#      Z$ G d de#      Z%e G d d             Z&e G d d             Z'e G d d             Z(d%dZ)edeee*ef      f   Z+	  G d d       Z,d&d!Z-d'd"Z.d(d#Z/	 	 	 	 	 	 d)d$Z0y)*u  QQ Bot chunked upload flow.

The QQ v2 API caps inline base64 uploads (``file_data`` / ``url``) at ~10 MB.
For files between 10 MB and ~100 MB we have to use the three-step chunked
upload flow::

    1. POST /v2/{users|groups}/{id}/upload_prepare
       → returns upload_id, block_size, and an array of pre-signed COS part URLs.
    2. For each part:
         PUT the part bytes to its pre-signed COS URL,
         then POST /v2/{users|groups}/{id}/upload_part_finish to acknowledge.
    3. POST /v2/{users|groups}/{id}/files with {"upload_id": ...}
       → returns the ``file_info`` token the caller uses in a RichMedia
       message.

Error-code semantics (from the QQ Bot v2 API spec):

- ``40093001`` — ``upload_part_finish`` retryable. Retry until the server-provided
  ``retry_timeout`` elapses (or a local cap).
- ``40093002`` — daily cumulative upload quota exceeded. Not retryable; surface
  as :class:`UploadDailyLimitExceededError` so the caller can build a
  user-friendly reply.

Exceptions:

- :class:`UploadDailyLimitExceededError` — daily quota hit (non-retryable).
- :class:`UploadFileTooLargeError` — file exceeds the platform per-file limit.
- :class:`RuntimeError` — generic upload failure (network, part PUT, complete).

Ported from WideLee's qqbot-agent-sdk v1.2.2 (``media_loader.py::ChunkedUploader``)
so the heavy-upload path stays in-tree. Authorship preserved via Co-authored-by.
    )annotationsN)	dataclass)Path)Any	AwaitableCallableDictListOptional)FILE_UPLOAD_TIMEOUTiJciIc   
   g     r@         ?g      ^@g     @g       @i  c                  8     e Zd ZdZdd fdZedd       Z xZS )UploadDailyLimitExceededErrorzRaised when ``upload_prepare`` returns biz_code 40093002.

    The daily cumulative upload quota for this bot has been reached. Callers
    should surface :attr:`file_name` + :attr:`file_size_human` so the model
    can compose a helpful reply.
    c                N    || _         || _        t        |   |xs d|       y )Nz Daily upload limit exceeded for )	file_name	file_sizesuper__init__)selfr   r   message	__class__s       Y/root/.hermes/venv/lib/python3.12/site-packages/gateway/platforms/qqbot/chunked_upload.pyr   z&UploadDailyLimitExceededError.__init__P   s.    ""G9)G	
    c                ,    t        | j                        S Nformat_sizer   r   s    r   file_size_humanz-UploadDailyLimitExceededError.file_size_humanW       4>>**r   ) )r   strr   intr   r%   returnNoner'   r%   )__name__
__module____qualname____doc__r   propertyr"   __classcell__r   s   @r   r   r   H   s!    
 + +r   r   c                  `     e Zd ZdZ	 	 d	 	 	 	 	 	 	 	 	 d fdZedd       Zedd       Z xZS )UploadFileTooLargeErrorz<Raised when a file exceeds the platform per-file size limit.c                    || _         || _        || _        |rdt        |       dnd}t        |   |xs d|dt        |       d|        y )Nz ()r$   zFile z) exceeds platform limit)r   r   limit_bytesr    r   r   )r   r   r   r5   r   	limit_strr   s         r   r   z UploadFileTooLargeError.__init___   sj     #"&8Cb[12!4	 	}B{9'=&> ?))25	
r   c                ,    t        | j                        S r   r   r!   s    r   r"   z'UploadFileTooLargeError.file_size_humanr   r#   r   c                H    | j                   rt        | j                         S dS )Nunknown)r5   r    r!   s    r   limit_humanz#UploadFileTooLargeError.limit_humanv   s     040@0@{4++,OiOr   )r   r$   )
r   r%   r   r&   r5   r&   r   r%   r'   r(   r)   )	r*   r+   r,   r-   r   r.   r"   r:   r/   r0   s   @r   r2   r2   \   sm    F 

 
 	

 
 

& + + P Pr   r2   c                  F    e Zd ZU dZded<   dZded<   dZded<   dZded<   y)_UploadProgressr   r&   total_partstotal_bytescompleted_partsuploaded_bytesN)r*   r+   r,   r=   __annotations__r>   r?   r@    r   r   r<   r<   }   s*    KKOSNCr   r<   c                  0    e Zd ZU ded<   ded<   dZded<   y)_PreparePartr&   indexr%   presigned_urlr   
block_sizeN)r*   r+   r,   rA   rG   rB   r   r   rD   rD      s    JJr   rD   c                  H    e Zd ZU ded<   ded<   ded<   eZded<   dZd	ed
<   y)_PrepareResultr%   	upload_idr&   rG   zList[_PreparePart]partsconcurrency        floatretry_timeoutN)r*   r+   r,   rA   _DEFAULT_CONCURRENT_PARTSrL   rO   rB   r   r   rI   rI      s&    NO0K0M5r   rI   c                   t        | j                  d      t              r| j                  d      n| }t        |j                  dd            }|st	        dt        |       dd        t        |j                  dd            }|j                  d	      xs |j                  d
      xs g }t        |t              r|st	        dt        |       dd        g }|D ]  }t        |t              s|j                  t        t        |j                  d      xs |j                  d      xs d      t        |j                  d      xs |j                  d      xs d      t        |j                  dd                          t        |||t        |j                  dt                    xs t        t        |j                  dd      xs d            S )zParse the upload_prepare API response into a normalized shape.

    The API may return the response directly or wrapped in ``data``.
    datarJ   r$   z+upload_prepare response missing upload_id: N   rG   r   rK   	part_listz'upload_prepare response missing parts: 
part_indexrE   rF   url)rE   rF   rG   rL   rO   rM   )rJ   rG   rK   rL   rO   )
isinstancegetdictr%   
ValueErrorr&   listappendrD   rI   rP   rN   )rawsrcrJ   rG   	raw_partsrK   ps          r   _parse_prepare_responsera      s   
 (>#''&/CCCGGK,-I9#c(4C.9IJ
 	
 SWW\1-.J >CGGK$8>BIi&i5c#htn5EF
 	
 !#E 
!T"!%%-DwD1E!EE/*@aeeEl@b quu\156	

 /HIJgNgCGGOS9@SA r   .c                      e Zd ZdZ	 d
	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 ddZ		 	 	 	 	 	 	 	 ddZ
y	)ChunkedUploaderu  Run the prepare → PUT parts → complete sequence.

    :param api_request: Bound ``_api_request(method, path, body=..., timeout=...)``
        coroutine from the adapter. Must raise ``RuntimeError`` with the biz_code
        embedded in the message on API errors.
    :param http_put: Coroutine ``(url, data, headers, timeout) -> response`` for
        COS part uploads. Typically wraps ``httpx.AsyncClient.put``.
    :param log_tag: Log prefix.
    c                .    || _         || _        || _        y r   )_api_request	_http_put_log_tag)r   api_requesthttp_putlog_tags       r   r   zChunkedUploader.__init__   s     (!r   c                  K   |dvrt        d|      t        |      }|j                         j                  }t        j                  d| j                  |t        |      |       t        j                         j                  dt        ||       d{   }| j                  ||||||       d{   }	t        |	j                  t              }
t        |	j                   dkD  r|	j                   nt"        t$              }t        j                  d| j                  |	j&                  t        |	j(                        t+        |	j,                        |
       t/        t+        |	j,                        |      }|	j,                  D cg c]?  }t1        j2                  | j4                  |||||	j&                  |	j(                  |||
      A }}t7        ||
       d{    t        j                  d	| j                  t+        |	j,                               | j9                  |||	j&                         d{   S 7 7 c c}w 7 h7 w)
u  Run the full chunked upload and return the ``complete_upload`` response.

        :param chat_type: ``'c2c'`` or ``'group'``.
        :param target_id: User or group openid.
        :param file_path: Absolute path to a local file.
        :param file_type: ``MEDIA_TYPE_*`` constant.
        :param file_name: Original filename (for upload_prepare).
        :returns: The raw response dict from ``complete_upload`` — contains
            ``file_info`` that the caller uses in a RichMedia message body.
        :raises UploadDailyLimitExceededError: On biz_code 40093002.
        :raises UploadFileTooLargeError: When the file exceeds the platform limit.
        :raises RuntimeError: On other API or I/O failures.
        >   c2cgroupz'ChunkedUploader: unsupported chat_type z2[%s] Chunked upload start: file=%s size=%s type=%dNr   zA[%s] Prepared: upload_id=%s block_size=%s parts=%d concurrency=%d)r=   r>   )		chat_type	target_id	file_pathr   rJ   rsp_block_sizepartrO   progressu)   [%s] All %d parts uploaded, completing…)rZ   r   statst_sizeloggerinforg   r    asyncioget_running_looprun_in_executor_compute_file_hashes_prepareminrL   _MAX_CONCURRENT_PARTSrO   _PART_FINISH_DEFAULT_TIMEOUT_PART_FINISH_MAX_TIMEOUTrJ   rG   lenrK   r<   	functoolspartial_upload_one_part_run_with_concurrency	_complete)r   rn   ro   rp   	file_typer   pathr   hashespreparemax_concurrentrO   rs   rr   taskss                  r   uploadzChunkedUploader.upload   s    * ,,9)G  IIIK''	@MM9k)&<i	
 //1AA&	9
 

 y)Y	6
 
 W002GH%,%:%:Q%>G!!D`$
 	OMM7,,k':L:L.M	
 #GMM*!
&  6
  %%####!++&11+!6
 6
 $E>:::7MM3w}}-	
 ^^Iy':K:KLLLa


(6
 	; MsW   BIH4I1H72CIAH:IH?AI/I0I7I:IIc                  K   |dk(  rdnd}| d| d}||||d   |d   |d   d	}		 | j                  d
||	t               d {   }
t        |
      S 7 # t        $ r(}t        |      }t         |v rt        |||      | d }~ww xY ww)Nrl   	/v2/users
/v2/groups/z/upload_preparemd5sha1md5_10m)r   r   r   r   r   r   POSTbodytimeout)re   r   RuntimeErrorr%   _BIZ_CODE_DAILY_LIMITr   ra   )r   rn   ro   r   r   r   r   baser   r   r]   excerr_msgs                r   r|   zChunkedUploader._prepare6  s      (50{lq?3"""%=6Ni(

	))41D *  C 's++  	#hG'(W43y' 	s:   $B	A AA B	A 	B#BBB	c
           	     ,  K   |j                   }
|j                  dkD  r|j                  n|}|
dz
  |z  }t        |||z
        }t        j                         j                  dt        |||       d{   }t        j                  |      j                         }t        j                  d| j                  |
|	j                  t        |      ||       | j                  |j                   ||
|	j                         d{    | j#                  ||||
|||       d{    |	xj$                  dz  c_        |	xj&                  |z  c_        t        j                  d| j                  |
|	j                  |	j$                  |	j                         y7 7 7 vw)z6PUT one part to COS, then call ``upload_part_finish``.r   r   Nz0[%s] Part %d/%d: uploading %s (offset=%d md5=%s)z"[%s] Part %d/%d done (%d/%d total))rE   rG   r}   rx   ry   rz   _read_file_chunkhashlibr   	hexdigestrv   debugrg   r=   r    _put_to_presigned_urlrF   _part_finish_with_retryr?   r@   )r   rn   ro   rp   r   rJ   rq   rr   rO   rs   rU   actual_block_sizeoffsetlengthrR   md5_hexs                   r   r   z ChunkedUploader._upload_one_partZ  sx     ZZ
/3/BDOOq.N2&	F(:; --/??"Ivv
 
 ++d#--/>MM:x';';	
 ((j(2F2F
 	
 	
 **y)
 	
 	

 	  A% 6)0MM:x';';$$h&:&:	
+
	
	
s8   A/F1F2B
F<F=FFA2FFFc                  K   d}t        t        dz         D ]  }	 t        j                  | j	                  ||dt        t        |            i      t               d{   }t        |dd      }d|cxk  rd	k  r(n n%t        j                  d
| j                  |||        yd}		 t        |dd      dd }	t        d| d|	        t        d| d| dt        dz    d|       7 # t        $ r Y =w xY w# t        $ rd}
|
}|t        k  rNdd|z  z  }t        j                  d| j                  |||dz   ||
       t        j                  |       d{  7   Y d}
~
Id}
~
ww xY ww)z1PUT part data to a pre-signed COS URL with retry.Nr   zContent-Length)rR   headers)r   status_coder   rS   i,  z[%s] PUT part %d/%d: %d OKr$   textzCOS PUT returned z: r   r   z9[%s] PUT part %d/%d attempt %d failed, retry in %.1fs: %szPart r   z upload failed after  attempts: )range_PART_UPLOAD_MAX_RETRIESrx   wait_forrf   r%   r   _PART_UPLOAD_TIMEOUTgetattrrv   r   rg   	Exceptionr   warningsleep)r   rV   rR   rU   r=   last_excattemptrespstatusbody_previewr   delays               r   r   z%ChunkedUploader._put_to_presigned_url  s     )-59: #	/G"/$--NN!!13s4y> B # 
 1  !}a8&&3&LL4z; !#*4#<Tc#BL #'xr,@ /#	/H J<q-B'!+,KzC
 	
E& ! 
  	/551<0ENNSz;!UC
 "--...	/s   E(AC8C' AC8"E($C8'C)7C8E('C8)	C52C84C55C88	E%AE EE E( E%%E(c           	       K   |dk(  rdnd}| d| d}	||||d}
t        j                         }|j                         }d}	 	 | j                  d|	|
t        	       d
{    y
7 # t
        $ r}t        |      }t         |vr |j                         |z
  }||k\  rt        d|dd| d|       ||dz  }t        j                  d| j                  |||       t        j                  t               d
{  7   Y d
}~nd
}~ww xY wɭw)z;Call ``upload_part_finish``, retrying on biz_code 40093001.rl   r   r   r   z/upload_part_finish)rJ   rU   rG   r   r   r   r   Nz4upload_part_finish persistent retry timed out after z.0fzs (z retries): r   z?[%s] part_finish retryable error, attempt %d, elapsed=%.1fs: %s)rx   ry   timere   r   r   r%   _BIZ_CODE_PART_RETRYABLErv   r   rg   r   _PART_FINISH_RETRY_INTERVAL)r   rn   ro   rJ   rU   rG   r   rO   r   r   r   loopstartr   r   r   elapseds                    r   r   z'ChunkedUploader._part_finish_with_retry  sM     (50{lq#67"$$	
 '')		A''Dt5H (       Ac(./?))+-m+&!!.s 33wi{3%Q  1(MM7GS
 mm$?@@@!A sO   A DA' A% A' $D%A' '	D0BD7C:8D=DDDc           	       K   |dk(  rdnd}| d| d}d|i}d}t        t        dz         D ]%  }	 | j                  d	||t        
       d{   c S  t        dt        dz    d|       7 # t        $ re}	|	}|t        k  rPt
        d|z  z  }
t        j                  d| j                  |dz   |
|	       t        j                  |
       d{  7   Y d}	~	d}	~	ww xY ww)zCall ``complete_upload`` with retry.

        This reuses the ``/files`` endpoint (same as the simple URL-based upload)
        but signals the chunked-completion path by sending only ``upload_id``.
        rl   r   r   r   z/filesrJ   Nr   r   r   r   z:[%s] complete_upload attempt %d failed, retry in %.1fs: %szcomplete_upload failed after r   )r   _COMPLETE_UPLOAD_MAX_RETRIESre   r   r   _COMPLETE_UPLOAD_BASE_DELAYrv   r   rg   rx   r   r   )r   rn   ro   rJ   r   r   r   r   r   r   r   s              r   r   zChunkedUploader._complete  s     (50{lq6*Y'(,9A=> 	/G/!..Dt5H /   	/ ++a/0H:G
 	
  	/9971<HENN-w{E3
 "--...	/sM   -CA.A,A.C,A..	C7ACCCCCCN)QQBot)rh   ApiRequestFnri   zCallable[..., Awaitable[Any]]rj   r%   r'   r(   )rn   r%   ro   r%   rp   r%   r   r&   r   r%   r'   Dict[str, Any])rn   r%   ro   r%   r   r&   r   r%   r   r&   r   Dict[str, str]r'   rI   )rn   r%   ro   r%   rp   r%   r   r&   rJ   r%   rq   r&   rr   rD   rO   rN   rs   r<   r'   r(   )
rV   r%   rR   bytesrU   r&   r=   r&   r'   r(   )rn   r%   ro   r%   rJ   r%   rU   r&   rG   r&   r   r%   rO   rN   r'   r(   )rn   r%   ro   r%   rJ   r%   r'   r   )r*   r+   r,   r-   r   r   r|   r   r   r   r   rB   r   r   rc   rc      s    	 !  0  	 
 
 SMSM SM 	SM
 SM SM 
SMr,, , 	,
 , , , 
,H-
-
 -
 	-

 -
 -
 -
 -
 -
 "-
 
-
^0
0
 0
 	0

 0
 
0
d-A-A -A 	-A
 -A -A -A -A 
-Af"
"
 "
 	"

 
"
r   rc   c                Z    t        |       }dD ]  }|dk  r
|dd| c S |dz  } |ddS )z>Return a human-readable file size string (e.g. ``'12.3 MB'``).)BKBMBGBg      @z.1f z TB)rN   )
size_bytessizeunits      r   r    r      sO    D' &=3Zq'' 3Zsr   c                    t        | d      5 }|j                  |       |j                  |      }t        |      |k7  r!t	        d|  d| d| dt        |       d	      |cddd       S # 1 sw Y   yxY w)zRead *length* bytes from *file_path* starting at *offset*.

    :raises IOError: If fewer bytes were read than expected (truncated file).
    rbzShort read from z: expected z bytes at offset z, got z (file may be truncated)N)openseekreadr   IOError)rp   r   r   fhrR   s        r   r   r     s    
 
i	 "
wwvt9"9+[ A D	{2JL    s   AA**A3c                   t        j                         }t        j                         }t        j                         }|t        kD  }d}t	        | d      5 }	 |j                  d      }|snU|j                  |       |j                  |       |r"t        |z
  }	|	dkD  r|j                  |d|	        |t        |      z  }i	 ddd       |j                         }
|
|j                         |r|j                         dS |
dS # 1 sw Y   AxY w)z0Compute md5, sha1, and md5_10m in a single pass.r   r   i   N)r   r   r   )	r   r   r   _MD5_10M_SIZEr   r   updater   r   )rp   r   r   r   r   need_10m
bytes_readr   chunk	remainingfull_md5s              r   r{   r{   /  s   
++-C<<>DkkmG=(HJ	i	 %"GGENEJJuKK)J6	q=NN5)#45#e*$J  	% }}H *27$$&	  9A	 % %s   A+D  D	c                   K   t        |d      }t        j                  |      dfdt        j                  fd| D          d{    y7 w)z=Run a list of thunks with a bounded number in flight at once.r   c                   K   4 d {     |         d {    d d d       d {    y 7 $7 7 	# 1 d {  7  sw Y   y xY wwr   rB   )thunksems    r   _wrapz$_run_with_concurrency.<locals>._wrapV  sF      	 	'MM	 	 		 	 	 	sE   A
/A
515A
3A
5A
A>AA
c              3  .   K   | ]  } |        y wr   rB   ).0tr   s     r   	<genexpr>z(_run_with_concurrency.<locals>.<genexpr>Z  s     3583s   N)r   zCallable[[], Awaitable[None]]r'   r(   )maxrx   	Semaphoregather)r   rL   r   r   s     @@r   r   r   N  sD     
 k1%K


K
(C ..3U3
444s   AA
AA)r]   r   r'   rI   )r   r&   r'   r%   )rp   r%   r   r&   r   r&   r'   r   )rp   r%   r   r&   r'   r   )r   z#List[Callable[[], Awaitable[None]]]rL   r&   r'   r(   )1r-   
__future__r   rx   r   r   loggingdataclassesr   pathlibr   typingr   r   r   r	   r
   r   !gateway.platforms.qqbot.constantsr   	getLoggerr*   rv   r   r   rP   r~   r   r   r   r   r   r   r   r   r   r   r2   r<   rD   rI   ra   r%   r   rc   r    r   r{   r   rB   r   r   <module>r      sT  B #     !  A A A			8	$ ! #      ! $     !  
+I +(Pi PB         $R YtCH~667H
 H
Z
 >5.55 
5r   