
    PL
j                    2   U 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ZddlZddlZddlZddlZddlZddlmZmZ ddlmZ ddlmZmZmZmZmZmZmZmZ ddlm Z!  e	j"        e#          Z$ddl%m&Z&m'Z'm(Z(m)Z)m*Z* dd	l+m,Z, dd
l-m.Z. dZ/dZ0dZ1dZ2dZ3dZ4dZ5dZ6dZ7dZ8dZ9dZ:dZ;dZ<dZ=dZ>dZ?dZ@dZAdZBdZCeDE                    d           ZF ejG        d!          ZH ejG        d"          ZI ejG        d#          ZJ ejG        d$          ZK ejG        d%ejL                  ZM ejG        d&ejN                  ZO ejG        d'ejN                  ZPdxd+ZQe5fdyd/ZRdzd5ZS G d6 d7ejT                  ZUe G d8 d9                      ZV G d: d;          ZW G d< d=          ZXd{dAZYd|dGZZ G dH dI          Z[d}dJZ\d~ddNZ]dddRZ^ddSZ_ddVZ`dWZadXebdY<   dd[Zcdd]ZddddaZe G db dce&          ZfddgZgddhZhddiZiddjZjddlZkddd^dmddtZlddvZmddwZndS )uF  
LINE Messaging API platform adapter for Hermes Agent.

A bundled platform plugin that runs an aiohttp webhook server, accepts LINE
webhook events (signature-verified), and relays messages to/from the agent
via the standard ``BasePlatformAdapter`` interface.

Design highlights
-----------------

**Reply token preferred, Push fallback.** LINE's reply token is single-use
and expires roughly 60 seconds after the inbound event. We try Reply first
(it's free) and fall back to the metered Push API when the token is absent,
expired, or rejected by the API.

**Slow-LLM postback button (optional).** When the LLM is still running past
``slow_response_threshold`` seconds (default 45, leaving 15s margin on the
60s reply-token TTL), we burn the original reply token to send a Template
Buttons bubble — the user taps it later to receive the cached answer via a
*fresh* reply token (also free). State machine: PENDING → READY → DELIVERED,
with ERROR for cancelled runs. Set the threshold to 0 to disable the
button and always Push-fallback instead.

**Three-allowlist gating.** Separate allowlists for users (U-prefixed),
groups (C-prefixed), and rooms (R-prefixed). ``LINE_ALLOW_ALL_USERS=true``
is a dev-only escape hatch.

**Media via public HTTPS.** LINE's Messaging API does *not* accept
binary uploads — images, audio, and video must be reachable HTTPS URLs.
We register registered tempfiles under ``/line/media/<token>/<filename>``
served by the same aiohttp app, with an allowed-roots traversal guard.
``LINE_PUBLIC_URL`` (e.g. ``https://my-tunnel.example.com``) overrides
the host:port construction so URLs are reachable when bind is 0.0.0.0
or behind a reverse proxy.

**5-message batching.** LINE accepts at most 5 message objects per
Reply/Push call; longer responses are smart-chunked at 4500 chars
(LINE per-bubble limit is 5000) and batched.

Synthesis credits
-----------------

This file is a synthesis of seven open community PRs adding LINE support
to Hermes Agent. It deliberately ports the *strongest* idea from each into
a single plugin-form module that requires zero core edits:

* PR #18153 (leepoweii)   — Template Buttons postback cache state machine,
  Markdown URL preservation, system-message bypass.
* PR #8398  (yuga-hashimoto) — media URL serving with traversal guard,
  send_voice / send_video, ``LINE_PUBLIC_URL`` env, macOS ``/tmp`` root.
* PR #16832 (jethac)      — config wiring style, voice/image tests.
* PR #21023 (perng)       — plugin-form skeleton (the only one already
  modeled on ``ADDING_A_PLATFORM.md``), reply→push fallback at 50s TTL,
  loading-animation indicator, source dispatcher.
* PR #14942 (soichiyo)    — Cloudflare-tunnel operating model (docs only).
* PR #14988 (David-0x221Eight) — text-first scope discipline.
* PR #6676  (liyoungc)    — Push-only mode (used as the ``threshold=0``
  fallback path here).
    )annotationsN)	dataclassfield)Path)Any	AwaitableCallableDictListOptionalSetTuple)quote)BasePlatformAdapterMessageEventMessageType
SendResultcache_image_from_bytes)Platform)SessionSourcez(https://api.line.me/v2/bot/message/replyz'https://api.line.me/v2/bot/message/pushz-https://api.line.me/v2/bot/chat/loading/startz<https://api-data.line.me/v2/bot/message/{message_id}/contentzhttps://api.line.me/v2/bot/infoi  i     2   i   i!  z/line/webhookz/line/mediag     F@uC   🤔 Still thinking. Tap below to fetch the answer when it's ready.
Get answeru   Already replied ✅z&Run was interrupted before completion.i  i   i  ڊ89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c63000100000005000100377a7ff20000000049454e44ae426082z"\[([^\]]+)\]\((https?://[^\s)]+)\)z\*\*(.+?)\*\*z#(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)z	`([^`]+)`z```[a-zA-Z0-9_+-]*\n?(.*?)```z
^#{1,6}\s+z^[\s]*[-*+]\s+textstrreturnc                   | s| S d
d}t                               ||           } t                              d|           } t                              d |           } t                              d|           } t
                              d|           } t                              d|           } t                              d	|           } | S )ul  Strip Markdown that LINE can't render, but keep URLs usable.

    LINE's text bubble has zero Markdown support — bold, italics, code
    fences, headings, and bullet markers all render as literal characters.
    URLs *are* auto-linked by the client, but only when they appear bare
    (not inside ``[label](url)`` syntax). This converts ``[label](url)``
    to ``label (url)`` so the URL remains tappable, then strips the rest.

    Source: PR #18153 (leepoweii) — adapted to keep code-block content
    visible (LINE users frequently want command snippets to land as
    plain text, not be eaten by the fence).
    mre.Matchr   r   c                R    |                      d                              d          S )N   
)grouprstripr   s    B/home/kuhnn/.hermes/hermes-agent/plugins/platforms/line/adapter.py_unfencez0strip_markdown_preserving_urls.<locals>._unfence   s     wwqzz  &&&    z\1c                ^    |                      d           d|                      d           dS )Nr"   z (   ))r$   r&   s    r'   <lambda>z0strip_markdown_preserving_urls.<locals>.<lambda>   s+    

%C%Caggajj%C%C%C r)    u   • )r   r    r   r   )_MD_CODE_BLOCK_REsub_MD_CODE_INLINE_RE_MD_LINK_RE_MD_BOLD_RE_MD_ITAL_RE_MD_HEADING_RE_MD_BULLET_RE)r   r(   s     r'   strip_markdown_preserving_urlsr7      s      ' ' ' '  400D !!%..D ??CCTJJD ??5$''D??5$''D b$''DVT**DKr)   	max_charsint	List[str]c                h   | sg S t          |           |k    r| gS g }| }|r#t          |          t          k     r
t          |          |k    r|                    |           d}n|                    dd|          }|t	          |dz            k     r|                    dd|          }|t	          |dz            k     r|                    dd|          }|dk    r|}|                    |d|                                                    ||d                                         }|rt          |          t          k     
|rk|rF|d         }t          |          |d	z
  k    r|d|d	z
           }|                                d
z   |d<   n#|                    |d|d	z
           d
z              |S )a  Split ``text`` into LINE-sized bubbles, preferring paragraph/line breaks.

    Returns at most ``LINE_MAX_MESSAGES_PER_CALL`` chunks; longer text is
    truncated with an ellipsis on the final chunk to keep the response
    deliverable in a single Reply/Push call.
    r.   z

r   g      ?r#    Nr"      …)lenLINE_MAX_MESSAGES_PER_CALLappendrfindr9   r%   lstrip)r   r8   chunks	remainingcuttails         r'   split_for_linerH      s     	
4yyIvFI
 -F&@@@y>>Y&&MM)$$$Ioofa33Y_%%%%//$955CY_%%%%//#q)44C!88Cio,,..///cddO**,,	  -F&@@@   > 	>":D4yy9q=((Oi!mO,.F2JJMM)Oi!mO4u<===Mr)   bodybytes	signaturechannel_secretboolc                :   |r|r| dS 	 t          j        |                    d          | t          j                                                  }t          j        |                              d          }n# t          $ r Y dS w xY wt          j
        ||          S )zVerify a LINE webhook's ``X-Line-Signature`` header.

    LINE signs the *raw* request body with HMAC-SHA256 keyed by the
    channel secret, then base64-encodes the digest. Constant-time
    comparison defends against timing oracles.
    NFutf-8)hmacnewencodehashlibsha256digestbase64	b64encodedecode	Exceptioncompare_digest)rI   rK   rL   rU   expecteds        r'   verify_line_signaturer\      s      N dlu!!'**N
 
 &((	 	
 #F++227;;   uux333s   A,A7 7
BBc                      e Zd ZdZdZdZdZdS )Statependingready	deliverederrorN)__name__
__module____qualname__PENDINGREADY	DELIVEREDERROR r)   r'   r^   r^     s"        GEIEEEr)   r^   c                      e Zd ZU ded<   dZded<   dZded<    eej        	          Zd
ed<    eej        	          Z	d
ed<   dS )_CacheEntryr^   stateNr   payloadr.   r   chat_id)default_factoryfloat
created_at
updated_at)
rc   rd   re   __annotations__rn   ro   r   timerr   rs   rj   r)   r'   rl   rl     s}         LLLGGdi888J8888di888J888888r)   rl   c                  X    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
d"dZdS )#RequestCacheu   In-memory cache for slow-LLM postback retrieval.

    PRs #18153 originally combined two TTLs — one for PENDING (24h) and
    a shorter one for READY/DELIVERED/ERROR (1h). We keep the same model
    here.
      Q ttl_secondsr9   pending_ttl_secondsr   Nonec                0    i | _         || _        || _        d S N)_entries_ttl_pending_ttl)selfrz   r{   s      r'   __init__zRequestCache.__init__%  s     
 13	/r)   ro   r   c                    t          t          j                              }t          t          j        |          | j        |<   |S )N)rm   ro   )r   uuiduuid4rl   r^   rf   r   )r   ro   rids      r'   register_pendingzRequestCache.register_pending.  s5    $*,,(u}gNNNc
r)   
request_idOptional[_CacheEntry]c                6    | j                             |          S r~   )r   get)r   r   s     r'   r   zRequestCache.get3  s    }  ,,,r)   rn   r   c                    | j                             |          }||j        t          j        urd S t          j        |_        ||_        t          j                    |_        d S r~   )	r   r   rm   r^   rf   rg   rn   ru   rs   )r   r   rn   entrys       r'   	set_readyzRequestCache.set_ready6  S    !!*--=EKu}<<Fk9;;r)   messagec                    | j                             |          }||j        t          j        urd S t          j        |_        ||_        t          j                    |_        d S r~   )	r   r   rm   r^   rf   ri   rn   ru   rs   )r   r   r   r   s       r'   	set_errorzRequestCache.set_error>  r   r)   c                    | j                             |          }||j        t          j        t          j        hvrd S t          j        |_        t          j                    |_        d S r~   )	r   r   rm   r^   rg   ri   rh   ru   rs   )r   r   r   s      r'   mark_deliveredzRequestCache.mark_deliveredF  sS    !!*--=EKU[/IIIFo9;;r)   Optional[str]c                    | j                                         D ]'\  }}|j        t          j        u r|j        |k    r|c S (d S r~   )r   itemsrm   r^   rf   ro   )r   ro   r   r   s       r'   find_pending_for_chatz"RequestCache.find_pending_for_chatM  sM    ---// 	 	JC{em++0H0H


tr)   c                D   t          j                     }d}t          | j                                                  D ]c}| j        |         }|j        t
          j        u r!||j        z
  | j        k    r| j        |= |dz  }C||j	        z
  | j
        k    r| j        |= |dz  }d|S )Nr   r"   )ru   listr   keysrm   r^   rf   rr   r   rs   r   )r   nowremovedr   r   s        r'   prunezRequestCache.pruneS  s    ikk**,,-- 		! 		!CM#&E{em++))D,===c*qLG))DI55c*qLGr)   N)rx   ry   )rz   r9   r{   r9   r   r|   )ro   r   r   r   )r   r   r   r   )r   r   rn   r   r   r|   )r   r   r   r   r   r|   )r   r   r   r|   )ro   r   r   r   )r   r9   )rc   rd   re   __doc__r   r   r   r   r   r   r   r   rj   r)   r'   rw   rw     s           #(0 0 0 0 0   
- - - -' ' ' '' ' ' '' ' ' '        r)   rw   c                  $    e Zd ZdZdddZddZdS )_MessageDeduplicatorzFBounded LRU of LINE webhook event IDs to ignore at-least-once retries.  max_sizer9   r   r|   c                "    i | _         || _        d S r~   )_seen_max)r   r   s     r'   r   z_MessageDeduplicator.__init__j  s    ')
			r)   event_idr   rM   c                p   |sdS || j         v rdS t          | j                   | j        k    rmt          | j                                                   t          | j                   dz  pd         fd| j                                         D             | _         t          j                    | j         |<   dS )NFT
   r"   c                (    i | ]\  }}|k    ||S rj   rj   ).0kvcutoffs      r'   
<dictcomp>z5_MessageDeduplicator.is_duplicate.<locals>.<dictcomp>v  s$    LLL41aV!Qr)   )r   r?   r   sortedvaluesr   ru   )r   r   r   s     @r'   is_duplicatez!_MessageDeduplicator.is_duplicaten  s     	5tz!!4tz??di''DJ--//00TZB1F1K!LFLLLL4:+;+;+=+=LLLDJ#y{{
8ur)   Nr   )r   r9   r   r|   )r   r   r   rM   )rc   rd   re   r   r   r   rj   r)   r'   r   r   g  sG        PP    
 
 
 
 
 
r)   r   sourceDict[str, Any]Tuple[str, str]c                    | pi                      dd          }|dk    r|                      dd          dfS |dk    r|                      dd          dfS |dk    r|                      dd          d	fS d
S )uy  Return ``(chat_id, chat_type)`` from a LINE event ``source`` block.

    LINE sources are one of:
      * ``{"type": "user",  "userId":  "U..."}``  → 1:1 DM
      * ``{"type": "group", "groupId": "C...", "userId": "U..."}``  → group chat
      * ``{"type": "room",  "roomId":  "R...", "userId": "U..."}``  → multi-user room

    Source: PR #21023 (perng), unchanged.
    typer.   r$   groupIdroomroomIduseruserIddm)r.   r   r   )r   src_types     r'   _resolve_chatr     s     "!!&"--H7zz)R(('116zz(B''//6zz(B''--8r)   	allow_alluser_idsSet[str]	group_idsroom_idsc               X   |rdS | pi                      dd          }|dk    r)|                      dd          }t          |          o||v S |dk    r)|                      dd          }t          |          o||v S |dk    r)|                      d	d          }t          |          o||v S d
S )u%   Three-list gate — credit PR #18153.Tr   r.   r   r   r$   r   r   r   F)r   rM   )	r   r   r   r   r   r   uidgidr   s	            r'   _allowed_for_sourcer     s      t"!!&"--H6jj2&&Cyy,SH_,7jjB''Cyy-SI--6jj2&&Cyy,SH_,5r)   c                  J    e Zd ZdZdddd
ZddZddZdddZd dZd!dZ	dS )"_LineClientzThin async wrapper around the LINE Messaging API.

    We use ``aiohttp`` directly to avoid a ``line-bot-sdk`` dependency
    (the SDK pulls in its own httpx pin and the ergonomic gain is small
    for the four endpoints we actually call).
    g      .@)timeoutchannel_access_tokenr   r   rq   r   r|   c               <    || _         || _        d| dd| _        d S )NBearer zapplication/json)AuthorizationContent-Type)_token_timeout_headers)r   r   r   s      r'   r   z_LineClient.__init__  s1    *=';==.
 
r)   reply_tokenmessagesList[Dict[str, Any]]c           
       K   dd l }|                    | j                  }|                    |d          4 d {V }|                    t
          | j        ||d          4 d {V 	 }|j        dk    r<|                                 d {V }t          d|j         d	|d d
                    	 d d d           d {V  n# 1 d {V swxY w Y   d d d           d {V  d S # 1 d {V swxY w Y   d S )Nr   totalTr   	trust_env)
replyTokenr   headersjson  zLINE reply :    )
aiohttpClientTimeoutr   ClientSessionpostLINE_REPLY_URLr   statusr   RuntimeError)r   r   r   r   r   sessionresprI   s           r'   replyz_LineClient.reply  s{     ''dm'<<((D(II 	R 	R 	R 	R 	R 	R 	RW||$/XFF $   R R R R R R R R ;#%%!%,,,,,,D&'PT['P'PD#J'P'PQQQQR R R R R R R R R R R R R R R R R R R R R R R R R R R	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R 	R6   ,C+,AC5C+
C	C+C	C++
C58C5ro   c           
       K   dd l }|                    | j                  }|                    |d          4 d {V }|                    t
          | j        ||d          4 d {V 	 }|j        dk    r<|                                 d {V }t          d|j         d	|d d
                    	 d d d           d {V  n# 1 d {V swxY w Y   d d d           d {V  d S # 1 d {V swxY w Y   d S )Nr   r   Tr   )tor   r   r   z
LINE push r   r   )
r   r   r   r   r   LINE_PUSH_URLr   r   r   r   )r   ro   r   r   r   r   r   rI   s           r'   pushz_LineClient.push  s{     ''dm'<<((D(II 	Q 	Q 	Q 	Q 	Q 	Q 	QW||#:: $   Q Q Q Q Q Q Q Q ;#%%!%,,,,,,D&'ODK'O'O4:'O'OPPPPQ Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q Q	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Q 	Qr   <   secondsr9   c                  K   |r|                     d          sdS ddl}t          dt          d|dz  dz  pd                    }	 |                    d          }|                    |d	          4 d{V }|                    t          | j        ||d
           d{V  ddd          d{V  dS # 1 d{V swxY w Y   dS # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)z@Loading indicator (DM only). LINE rejects this for groups/rooms.UNr   r   r   g      @r   Tr   )chatIdloadingSecondsr   z!LINE loading indicator failed: %s)
startswithr   maxminr   r   r   LINE_LOADING_URLr   rY   loggerdebug)r   ro   r   r   clampedr   r   excs           r'   loadingz_LineClient.loading  s      	g0055 	FaR'Q,!!3!8q99::		C++#+66G,,W,MM       QXll$ M$+wGG #                                        	C 	C 	CLL<cBBBBBBBBB	Cs<   3C :,B9&C 9
CC CC 
C<C77C<
message_idrJ   c                ,  K   ddl }t                              |          }|                    d          }|                    |d          4 d{V }|                    |dd	| j         i
          4 d{V }|j        dk    rt          d|j                   |	                                 d{V cddd          d{V  cddd          d{V  S # 1 d{V swxY w Y   	 ddd          d{V  dS # 1 d{V swxY w Y   dS )z3Download an inbound media message's binary content.r   N)r  g      >@r   Tr   r   r   r   r   zLINE content )
r   LINE_CONTENT_URL_FMTformatr   r   r   r   r   r   read)r   r  r   urlr   r   r   s          r'   fetch_contentz_LineClient.fetch_content  sp     "))Z)@@''d'33((D(II 	) 	) 	) 	) 	) 	) 	)W{{3BYDKBYBY0Z{[[ ) ) ) ) ) ) )_c;#%%&'Dt{'D'DEEE!YY[[(((((() ) ) ) ) ) ) ) ) ) ) ) )	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)) ) ) ) ) ) ) ) ) ) ) ) ) ) )	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)s6   (D=<C9D
C(	(D+C(	,D
DDr   c                `  K   ddl }|                    d          }	 |                    |d          4 d{V }|                    t          | j                  4 d{V }|j        dk    r%	 ddd          d{V  ddd          d{V  dS |                                 d{V }|                    d	          cddd          d{V  cddd          d{V  S # 1 d{V swxY w Y   	 ddd          d{V  dS # 1 d{V swxY w Y   dS # t          $ r Y dS w xY w)
z?Fetch this channel's own userId so we can filter self-messages.r   Ng      $@r   Tr   r  r   r   )	r   r   r   r   LINE_BOT_INFO_URLr   r   r   rY   )r   r   r   r   r   datas         r'   get_bot_user_idz_LineClient.get_bot_user_id  s     ''d'33	,,W,MM . . . . . . .QX";;'8$-;PP . . . . . . .TX{c))#. . . . . . . . . . . .. . . . . . . . . . . . . . "&,,,,,,D88H--	. . . . . . . . . . . . .. . . . . . . . . . . . . .. . . . . . . . . . . . . . .. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .  	 	 	44	sp   D (D#C'0DD .C'DD '
C1	1D4C1	5D9D 
DD DD 
D-,D-N)r   r   r   rq   r   r|   )r   r   r   r   r   r|   )ro   r   r   r   r   r|   )r   )ro   r   r   r9   r   r|   )r  r   r   rJ   )r   r   )
rc   rd   re   r   r   r   r   r  r  r  rj   r)   r'   r   r     s          GK 
 
 
 
 
 
R R R RQ Q Q QC C C C C$	) 	) 	) 	)     r)   r   c                f    t          |           t          k    r| dt          dz
           dz   } d| dS )z;Build a LINE text message object, capped to per-bubble max.Nr"   r>   r   )r   r   )r?   LINE_PER_BUBBLE_CHARS)r   s    r'   _text_messager  	  s=    
4yy(((/+a//058D)))r)   original_urlpreview_urlr   c                    d| |p| dS )Nimager   originalContentUrlpreviewImageUrlrj   )r  r  s     r'   _image_messager    s    *&6,  r)   r   r
  duration_msc                (    d| t          |          dS )Naudio)r   r  duration)r9   )r
  r  s     r'   _audio_messager    s"    !$$  r)   c                    d| |dS )Nvideor  rj   )r
  r  s     r'   _video_messager"     s    !&  r)   button_labelr   c                    t          |           dk    r| n| dd         dz   }t          |           dk    r| n| dd         dz   }d|d|d	|dd
         pdt          j        d|d          |dd         pddgddS )u<  Template Buttons message — the slow-LLM postback bubble.

    From PR #18153 (leepoweii). Template Buttons stay tappable from chat
    history, unlike Quick Reply chips which are dismissed the moment any
    new message arrives in the chat.

    LINE limits: ``text`` ≤ 160 chars, ``altText`` ≤ 400 chars.
       N   z...r   i  templatebuttonspostback   r   show_response)actionr   i,  )r   labelr  displayText)r   r   actions)r   altTextr'  )r?   r   dumps)r   r#  r   	truncatedalts        r'   build_postback_button_messager4  (  s     D		S((d4C4j5.@Id))s""$$TcT
U(:C ')#2#.>, J#2*MM  $0#5#E 	
 
  r)   )u   ⚡ Interruptingu
   ⏳ Queuedu   ⏩ Steeredu   💾zTuple[str, ...]_SYSTEM_BYPASS_PREFIXEScontentc                L      sdS t           fdt          D                       S )NFc              3  B   K   | ]}                     |          V  d S r~   )r   )r   pr6  s     r'   	<genexpr>z$_is_system_bypass.<locals>.<genexpr>X  s1      FFw!!!$$FFFFFFr)   )anyr5  )r6  s   `r'   _is_system_bypassr<  U  s4     uFFFF.EFFFFFFr)   valuec                `    | st                      S d |                     d          D             S )Nc                ^    h | ]*}|                                 |                                 +S rj   )strip)r   xs     r'   	<setcomp>z_csv_set.<locals>.<setcomp>b  s-    ===!17799=AGGII===r)   ,)setsplit)r=  s    r'   _csv_setrF  _  s3     uu==u{{3//====r)   Fnamedefaultc                    t          j        |           }||S |                                                                dv S )N>   1onyestrue)osgetenvr@  lower)rG  rH  r   s      r'   _truthy_envrQ  e  s7    
	$Ay7799?? :::r)   c                      e Zd ZdZ fdZd>dZd?dZd@d	Zd@d
ZdAdZ	dAdZ
dAdZdBdZ	 	 dCdDdZdEdZdFd ZdGdHd!ZdId"ZdJd#ZdH fd$ZdK fd&Zd'd(dLd+ZdMd.Zd@d/Z	 	 dCdNd2Z	 	 dOdPd7Z	 	 dCdQd:ZdRd=Z xZS )SLineAdapterz#LINE Messaging API gateway adapter.c           	        t          d          }t                                          ||           t          |di           pi }t	          j        d          p|                    dd          | _        t	          j        d          p|                    dd          | _        t	          j        d	          p|                    d
d          | _	        	 t          t	          j        d          p|                    dt                              | _        n## t          t          f$ r t          | _        Y nw xY w|                    dt                    | _        t	          j        d          p|                    dd          pd                    d          | _        t'          dt)          |                    dd                              | _        t-          t	          j        dd                    t/          |                    dg                     z  | _        t-          t	          j        dd                    t/          |                    dg                     z  | _        t-          t	          j        dd                    t/          |                    dg                     z  | _        	 t7          t	          j        d          p|                    dt8                              | _        n## t          t          f$ r t8          | _        Y nw xY wt	          j        d          p|                    dt<                    | _        t	          j        d          p|                    d t@                    | _!        t	          j        d!          p|                    d"tD                    | _#        t	          j        d#          p|                    d$tH                    | _%        d | _&        d | _'        d | _(        d | _)        i | _*        tW                      | _,        t[                      | _.        d | _/        d | _0        i | _1        t/                      | _2        tf          | _4        i | _5        d S )%Nline)configplatformextraLINE_CHANNEL_ACCESS_TOKENr   r.   LINE_CHANNEL_SECRETrL   	LINE_HOSThost0.0.0.0	LINE_PORTportwebhook_pathLINE_PUBLIC_URL
public_url/LINE_ALLOW_ALL_USERSallow_all_usersFLINE_ALLOWED_USERSallowed_usersLINE_ALLOWED_GROUPSallowed_groupsLINE_ALLOWED_ROOMSallowed_roomsLINE_SLOW_RESPONSE_THRESHOLDslow_response_thresholdLINE_PENDING_TEXTpending_textLINE_BUTTON_LABELr#  LINE_DELIVERED_TEXTdelivered_textLINE_INTERRUPTED_TEXTinterrupted_text)6r   superr   getattrrN  rO  r   r   rL   webhook_hostr9   DEFAULT_WEBHOOK_PORTwebhook_port	TypeError
ValueErrorDEFAULT_WEBHOOK_PATHr`  r%   public_base_urlrQ  rM   r   rF  rD  rg  ri  rk  rq   DEFAULT_SLOW_RESPONSE_THRESHOLDrm  DEFAULT_PENDING_REPLY_TEXTro  DEFAULT_BUTTON_LABELr#  DEFAULT_DELIVERED_TEXTrr  DEFAULT_INTERRUPTED_TEXTrt  _client_app_runner_site_reply_tokensrw   _cacher   _dedup_bot_user_id	_lock_key_media_tokens_media_temp_pathsMEDIA_TOKEN_TTL_SECONDS
_media_ttl_pending_buttons)r   rV  kwargsrW  rX  	__class__s        r'   r   zLineAdapter.__init__v  s2   F##:::,,2 I122 5yy/44 	!
 I+,, /yy)2.. 	 Ik22Reii	6R6R	5 #	+&&Q%))F<P*Q*Q! !D :& 	5 	5 	5 4D	5!IIn6JKK
 I'(( yyr**
&++	 	 %"D3De)L)L$M$M
 
 &I*B//
 
		/2..//0 'I+R00
 
		*B//001 &I*B//
 
		/2..//0
	K+0	899 Y9968WXX, ,D(( :& 	K 	K 	K+JD(((	K
 I)** Eyy)CDD 	
 I)** ?yy)=>> 	
 I+,, Cyy)+ABB 	
 I-.. Gyy+-EFF 	 /3	
;="nn*,,+/(, <>+.551 13s&   AD D76D76AK8 8LLr   rM   c           
     V  K   | j         r| j        s|                     ddd           dS 	 ddlm} t          j        | j                                                                                   d d         } |d|          s|                     d	d
d           dS || _	        n# t          $ r
 d | _	        Y nw xY wt          | j                   | _        	 | j                                         d {V | _        n9# t          $ r,}t                               d|           d | _        Y d }~nd }~ww xY w	 ddlm} n)# t          $ r |                     ddd           Y dS w xY w|                    t*                    | _        | j        j                            | j        | j                   | j        j                            | j         d| j                   | j        j                            t:           d| j                   |                    | j                  | _         	 | j         !                                 d {V  |"                    | j         | j#        | j$                  | _%        | j%        &                                 d {V  nC# tN          $ r6}|                     dd| j#         d| j$         d| d           Y d }~dS d }~ww xY w| (                                 t           )                    d| j#        | j$        | j        | j*        rd| j*         dnd           dS )Nconfig_missingz=LINE_CHANNEL_ACCESS_TOKEN and LINE_CHANNEL_SECRET must be setF)	retryabler   )acquire_scoped_lock   rU  lock_conflictz.LINE channel already in use by another profilez LINE: get_bot_user_id failed: %swebmissing_depuO   aiohttp is required for the LINE adapter — install with `pip install aiohttp`)client_max_sizez/healthz/{token}/{filename}bind_failedzCould not bind LINE webhook on :r   Tz$LINE: webhook listening on %s:%s%s%sz
 (public: r,   r.   )+r   rL   _set_fatal_errorgateway.statusr  rS   rT   rR   	hexdigestr  ImportErrorr   r  r  r  rY   r   r   r   r  ApplicationWEBHOOK_BODY_MAX_BYTESr  routeradd_postr`  _handle_webhookadd_get_handle_healthDEFAULT_MEDIA_PATH_PREFIX_handle_media	AppRunnerr  setupTCPSiterw  ry  r  startOSError_mark_connectedinfor}  )r   r  tok_hashr  r  s        r'   connectzLineAdapter.connect  s     ( 	0C 	!! O "   
 5	"::::::~d&?&F&F&H&HIISSUUVYWYVYZH&&vx88 %%#D# &   
 u%DNN 	" 	" 	"!DNNN	" #4#<==	%&*l&B&B&D&D D D D D D DD 	% 	% 	%LL;SAAA $D	%
	####### 	 	 	!!a "   
 55	 OO4JOKK		!!$"3T5IJJJ	  D$5!>!>!>@STTT	  (AAA	
 	
 	

 }}TY//
	,$$&&&&&&&&&T\43DdFWXXDJ*""$$$$$$$$$$ 	 	 	!!`$2C``dFW``[^`` "   
 55555	 	2484HP0-0000b	
 	
 	
 ts[   A/B% B% %B98B9$C; ;
D1"D,,D15D< <"E"!E")A)J 
K+KKr|   c                  K   |                                   | j        8	 | j                                         d {V  n# t          $ r Y nw xY wd | _        | j        8	 | j                                         d {V  n# t          $ r Y nw xY wd | _        d | _        t          | j                  D ]'}	 t          j
        |           # t          $ r Y $w xY w| j                                         | j                                         | j        r2	 ddlm}  |d| j                   n# t          $ r Y nw xY wd | _        d S d S )Nr   )release_scoped_lockrU  )_mark_disconnectedr  stoprY   r  cleanupr  r   r  rN  unlinkr  clearr  r  r  r  )r   pathr  s      r'   
disconnectzLineAdapter.disconnect*  s     !!!:!joo''''''''''   DJ<#l**,,,,,,,,,,   DL	 /00 	 	D	$   $$&&&  """> 	">>>>>>##FDN;;;;   !DNNN	" 	"sE   ? 
AAA> >
B
B3C
CCD* *
D76D7r   c                B   K   ddl m} |                    ddd          S )Nr   r  okrU  )r   rW  )r   r  json_response)r   requestr  s      r'   r  zLineAdapter._handle_healthP  s4        Df!E!EFFFr)   c                v  K   ddl m} 	 |                                 d {V }nI# t          $ r<}t                              d|           |                    dd          cY d }~S d }~ww xY wt          |          t          k    r|                    dd          S |j	        
                    d	d
          }t          ||| j                  s|                    dd          S 	 t          j        |                    d                    }n3# t           t          j        f$ r |                    dd          cY S w xY w|
                    dg           pg }|D ]H}	 |                     |           d {V  # t          $ r t                              d           Y Ew xY w|                    dd          S )Nr   r  zLINE: read failed: %sr   zbad requestr   r   i  zpayload too largezX-Line-Signaturer.   i  zinvalid signaturerO   zbad jsoneventszLINE: dispatch_event failedr   r  )r   r  r	  rY   r   r   Responser?   r  r   r   r\   rL   r   loadsrX   UnicodeDecodeErrorJSONDecodeError_dispatch_event	exception)	r   r  r  rI   r  rK   rn   r  events	            r'   r  zLineAdapter._handle_webhookT  s     	@ ''''''DD 	@ 	@ 	@LL0#666<<s<????????	@ t99---<<s1D<EEEO''(:B??	$T9d6IJJ 	F<<s1D<EEE	=jW!5!566GG"D$89 	= 	= 	=<<s<<<<<<	= Xr**0b 	@ 	@E@**51111111111 @ @ @  !>?????@ ||3T|222s>   % 
A+1A& A+&A+&'D -D>=D>E::$F! F!r  r   c                  K   |                     d          }|                     d          pi }|                     dd          pd}|r7| j                            |          rt                              d|           d S |                     dd          }| j        r|| j        k    rd S t          || j        | j        | j	        | j
                  st                              d|           d S |d	k    r|                     |           d {V  d S |d
k    r|                     |           d {V  d S |dv rt                              d||           d S t                              d|           d S )Nr   r   webhookEventIdr.   z)LINE: ignoring duplicate webhook event %sr   )r   r   r   r   z&LINE: rejecting unauthorized source %sr   r)  >   joinleavefollowunfollowz LINE: lifecycle event %s from %szLINE: ignoring event type %r)r   r  r   r   r   r  r   r   rg  ri  rk  r  _handle_message_event_handle_postback_event)r   r  
event_typer   webhook_event_idsender_user_ids         r'   r  zLineAdapter._dispatch_events  s     YYv&&
8$$* 99%5r::@b  	 8 89I J J 	LLDFVWWWF  Hb11 	43D!D!DF #n')'
 
 
 	 KK@&IIIF"",,U33333333333:%%--e44444444444BBBKK:JOOOOOLL7DDDDDr)   c           	       K   |                     d          pi }|                     dd          }|                     dd          }|                     dd          }|                     d          pi }t          |          \  }}|                     dd          p|}	|r'|r%|t          j                    t          z   f| j        |<   g }
g }d}|dk    r|                     dd          pd}n|d	v rO|                     ||           d {V }|r*|
                    |           |                    |           d
| d}n|dk    r5|                     d          pg }|rdd                    |           dnd}nT|dk    rH|                     dd          }|                     dd          }d| d| d                                }nd| d}|dk    r3| j	        r,t          j        | j	                            |                     |                     |||	|	|          }t          ||dk    rt          j        nt          j        ||||
|          }|                     |           d {V  d S )Nr   r   r.   idr   r   r   r   >   filer  r  r!  []stickerkeywordsz
[sticker: z, z	[sticker]locationtitleaddressz[location: r<   z[unsupported message type: r   )ro   	chat_typeuser_id	user_name	chat_name)r   message_typer   raw_messager  
media_urlsmedia_types)r   r   ru   LINE_REPLY_TOKEN_TTL_SECONDSr  _download_mediarA   r  r@  r  asynciocreate_taskr  build_sourcer   r   TEXTIMAGEhandle_message)r   r  msgmsg_typer  r   r   ro   r  r  r  r  r   
local_pathr  r  r  
source_obj	event_objs                      r'   r  z!LineAdapter._handle_message_event  s     ii	""(b7762&&WWT2&&
iib118$$**622**Xr**5g  	{ 		::+Dw' !#
!#v7762&&,"DD<<<#33JIIIIIIIIJ -!!*---""8,,,"x???DD""wwz**0bH:BS6		( 3 36666DD##GGGR((Eggi,,G33333399;;DD<<<<D  4 4W = =>>>&& ' 
 

 !-5-?-?))[EV!!#
 
 
	 !!),,,,,,,,,,,r)   c                  K   |                     d          pi }|                     dd          pd}|                     dd          }|                     d          pi }t          |          \  }}	 t          j        |          }n# t          t          j        f$ r Y dS w xY w|                     d          dk    rdS |                     d	d          }	|	sdS | j                             |	          }
| j        r|r|
sdS |
j        t          j
        u rg|
j        pd}t          t          t          |                              }d
 |D             dt                   }	 | j                            ||           d{V  | j                            |	           | j                            |d           dS # t(          $ r}t*                              d|           	 | j                            ||           d{V  | j                            |	           | j                            |d           n2# t(          $ r%}t*                              d|           Y d}~nd}~ww xY wY d}~dS Y d}~dS d}~ww xY w|
j        t          j        u rt          |
j        p| j                  }	 | j                            |t7          |          g           d{V  | j                            |	           | j                            |d           dS # t(          $ r&}t*                              d|           Y d}~dS d}~ww xY w|
j        t          j        u rH	 | j                            |t7          | j                  g           d{V  dS # t(          $ r Y dS w xY w|
j        t          j        u rH	 | j                            |t7          | j                  g           d{V  dS # t(          $ r Y dS w xY wdS )uD   User tapped the slow-LLM postback button — deliver cached payload.r)  r  r.   r   r   Nr,  r+  r   c                ,    g | ]}t          |          S rj   r  r   cs     r'   
<listcomp>z6LineAdapter._handle_postback_event.<locals>.<listcomp>  s     999Qa((999r)   z6LINE: postback reply failed (%s); falling back to pushz'LINE: postback push fallback failed: %sz%LINE: postback ERROR reply failed: %s) r   r   r   r  rz  r  r  r  rm   r^   rg   rn   rH   r7   r   r@   r   r   r  poprY   r   warningr   rb   ri   rt  r  rh   rr  rf   ro  )r   r  r)  r  r   r   ro   _parsedr   r   rn   rD   r   r  exc2r   s                    r'   r  z"LineAdapter._handle_postback_event  s     99Z((.B||FB''-2iib118$$*"6**
	Z%%FF4/0 	 	 	FF	 ::h?22FZZb11
 	F
++| 	; 	e 	F;%+%%m)rG#$B3w<<$P$PQQF99&999:U;U:UVHRl((h?????????**:666%))'488888 R R RWY\]]]R,++GX>>>>>>>>>K..z:::)--gt<<<<  R R RLL!JDQQQQQQQQR =<<<<<QQQQQQR [EK''u}=(=>>DMl((}T7J7J6KLLLLLLLLL**:666%))'488888 M M MFLLLLLLLLLM[EO++l((}TEX7Y7Y6Z[[[[[[[[[[[   [EM))l((}TEV7W7W6XYYYYYYYYYYY   	 *)s   2B B! B! AF8 8
I8I3AH54I35
I$?II3I$$I33I8*A$L 
M L;;M 4N 
NN24O( (
O65O6r  r   r  r   c                  K   | j         r|sd S 	 | j                             |           d {V }n5# t          $ r(}t                              d|||           Y d }~d S d }~ww xY wddddd                    |d          }	 t          ||          S # t          $ r'}t                              d||           Y d }~d S d }~ww xY w)	Nz+LINE: failed to fetch %s content for %s: %sz.jpgz.m4az.mp4z.bin)r  r  r!  r  )extz$LINE: failed to cache %s payload: %s)r  r  rY   r   r  r   r   )r   r  r  r  r  r  s         r'   r  zLineAdapter._download_media  s     | 	: 	4	33J????????DD 	 	 	NNH(T^`cddd44444	 	
 

 #h

 		)$C8888 	 	 	NNA8SQQQ44444	s,    0 
A"AA"B 
CB>>CNro   r6  reply_tometadataOptional[Dict[str, Any]]r   c                \  K   | j         st          dd          S t          |          r|                     ||d           d {V S | j                            |          }|r,| j                            ||           t          d|          S |                     ||d           d {V S )NFLINE adapter not connectedsuccessrb   )
force_pushTr	  r  )r  r   r<  _send_text_chunksr  r   r  r   )r   ro   r6  r  r  pending_rids         r'   sendzLineAdapter.send)  s       | 	Qe3OPPPP
 W%% 	T//U/SSSSSSSSS +//88 	DK!!+w777d{CCCC++GW+OOOOOOOOOr)   r
  c                 K   | j         st          dd          S t          t          |                    }|st          dd           S d |D             d t                   }|                     |          \  }}|rg|se	 | j                             ||           d {V  t          d|          S # t          $ r%}t          	                    d|           Y d }~nd }~ww xY w	 | j         
                    ||           d {V  t          dd           S # t          $ rC}t                              d|           t          dt          |                    cY d }~S d }~ww xY w)	NFr  r  Tr  c                ,    g | ]}t          |          S rj   r  r  s     r'   r  z1LineAdapter._send_text_chunks.<locals>.<listcomp>O  s     555M!$$555r)   5LINE: reply token rejected (%s); falling back to pushzLINE: push send failed: %s)r  r   rH   r7   r@   _consume_reply_tokenr   rY   r   r  r   rb   r   )	r   ro   r6  r
  rD   r   token
used_replyr  s	            r'   r  zLineAdapter._send_text_chunksB  s      | 	Qe3OPPPP >w G GHH 	=dt<<<<55f5556Q7Q6QR 55g>>z 	Zj 	ZZl((999999999!$5AAAA Z Z ZSUXYYYYYYYYZ	=,##GX666666666dt<<<< 	= 	= 	=LL5s;;;e3s88<<<<<<<<<	=s6    1B2 2
C!<CC!%1D 
E$!8EE$E$Tuple[str, bool]c                    | j                             |d          }|sdS |\  }}|rt          j                    |k    rdS |dfS )zjConsume a stashed reply token if present and unexpired.

        Returns ``(token, used_reply)``.
        N)r.   FT)r  r  ru   )r   ro   r   r  
expires_ats        r'   r  z LineAdapter._consume_reply_tokena  sZ    
 "&&w55 	9!z 		z119d{r)   c                d   K   | j         r$|r$| j                             |           d{V  dS dS dS )z5Trigger LINE's loading-animation indicator (DM only).N)r  r  )r   ro   r  s      r'   send_typingzLineAdapter.send_typingn  sY      < 	0G 	0,&&w///////////	0 	0 	0 	0r)   c                `   K   |pddd         }dddd                     |d          }|pd|dS )	uH  Best-effort chat info derived from the chat_id prefix.

        LINE's chat-info APIs are limited and per-source-type — instead of
        chasing them we infer from the well-known ID prefixes:
        ``U`` = user (1:1), ``C`` = group, ``R`` = room. The agent only
        needs ``name`` + ``type`` from this method.
        r.   Nr"   r   r$   channel)r   CR)rG  r   r   )r   ro   prefixr  s       r'   get_chat_infozLineAdapter.get_chat_infos  sK       -R!$W9==AA&$OO	2y999r)   c                     t          |          S )z:Strip Markdown that LINE can't render. URLs are preserved.)r7   )r   r6  s     r'   format_messagezLineAdapter.format_message  s    -g666r)   c                V   K    j         dk    s	 j        rs% t                      j        g|R i | d{V  dS d fd}t	          j         |                      }	  t                      j        g|R i | d{V  |                                s<|                                 	 | d{V  dS # t          j        t          f$ r Y dS w xY wdS # |                                s:|                                 	 | d{V  w # t          j        t          f$ r Y w w xY ww xY w)a  Override the base loop to fire the postback button at threshold.

        We intentionally keep the base implementation behind us: it's
        responsible for the typing-indicator heartbeat, while *this*
        wrapper layers in the slow-LLM postback bubble at threshold.
        r   Nr   r|   c                   K   	 t          j        j                   d {V  n# t           j        $ r  w xY wj        vrd S j        v rd S j                                      } | j        <                                 \  }}|sj        	                    d            d S t          j        j        |           }	 j                            ||g           d {V  t                              d|            d S # t"          $ rA}t                              d|           j        	                    d            Y d }~d S d }~ww xY w)Nz8LINE: sent slow-LLM postback button for chat %s (rid=%s)z%LINE: postback button send failed: %s)r  sleeprm  CancelledErrorr  r  r  r   r  r  r4  ro  r#  r  r   r   r  rY   r  )r   r  usedr  r  ro   r   s        r'   _fire_postbackz0LineAdapter._keep_typing.<locals>._fire_postback  s     mD$@AAAAAAAAAA)    d000$///+..w77C-0D!'*33G<<KE4 %))'4888/!4#4c C9l((666666666VX_adeeeee 9 9 9FLLL%))'48888888889s    % 6>D 
E6EEr   r|   )
rm  r  ru  _keep_typingr  r  donecancelr%  rY   )r   ro   argsr  r'  	post_taskr  s   ``    r'   r)  zLineAdapter._keep_typing  s      (A--< . . '%''&w@@@@@@@@@@@@@F	9 	9 	9 	9 	9 	9 	96 '(8(899		&%''&w@@@@@@@@@@@@@>>##   """#OOOOOOOOO.	:   DD	 9>>##   """#OOOOOOOO.	:   D	sB   ##C /B9 9CC)D(D
D(D$!D(#D$$D(session_keyc                   K   t                                          ||           d{V  | j                            |d          }|r"| j                            || j                   dS dS )z?Resolve any orphan PENDING postback so the button doesn't loop.N)ru  interrupt_session_activityr  r  r  r   rt  )r   r.  ro   r   r  s       r'   r0  z&LineAdapter.interrupt_session_activity  s~      gg00gFFFFFFFFF#''66 	>K!!#t'<=====	> 	>r)   Fr  	file_pathr  c               V   t          j                     }t          | j                                                  D ]|}| j        |         \  }}||k    rd| j                            |d           || j        v r@| j                            |           	 t          j        |           l# t          $ r Y xw xY w}t          t          |                                                    }t          j        d          }||| j        z   f| j        |<   |r| j                            |           |S )z>Register a local file for HTTPS serving; return the URL token.N    )ru   r   r  r   r  r  discardrN  r  r  r   r   resolvesecretstoken_urlsafer  add)r   r2  r  r   r  r  expresolveds           r'   _register_mediazLineAdapter._register_media  s0    ikk$,113344 		 		E*51ID#Syy"&&ud3334111*224888	$"    tI..0011%b))%-sT_/D$E5! 	1"&&x000s   B&&
B32B3r  filenamec                    | j         r| j         }n"| j        }| j        }|dk    rd| }nd| d| }t          |d          }| t           d| d| S )z=Build the public HTTPS URL for a media token. PR #8398 style.i  https://r  r.   )saferc  )r}  rw  ry  	_urlquoter  )r   r  r=  baser\  r_  	safe_names          r'   
_media_urlzLineAdapter._media_url  s     	0'DD$D$Ds{{($((/$////hR000	G1GGEGGIGGGr)   c                  K   ddl m} |j        d         }| j                            |          }|s|                    dd          S |\  }}t          j                    |k    r2| j                            |d           |                    dd	          S t          |          }|	                                r|
                                s|                    dd          S 	 dd
lm} t           |                                                      }	nH# t          $ r; t          j                                        d                                          }	Y nw xY wt          t#          j                                                              t          d                                          |	h}
|                                t'          fd|
D                       s2t(                              d           |                    dd          S t-          j        t1          |                    \  }}|                    |d|pdi          S )a  Serve a registered local file over HTTPS for LINE's media URLs.

        Defence-in-depth: even though ``_register_media`` is only called
        from trusted internal code, we recheck the resolved path against
        an allowed-roots set before serving. Sources allowed:
        ``tempfile.gettempdir()``, ``/tmp`` (which resolves to
        ``/private/tmp`` on macOS), and ``HERMES_HOME``. PR #8398.
        r   r  r  i  z	not foundr  Ni  gone)get_hermes_homez.hermesz/tmpc              3  8   K   | ]}t          |          V  d S r~   )_is_relative_to)r   rr;  s     r'   r:  z,LineAdapter._handle_media.<locals>.<genexpr>  s-      GGA?8Q//GGGGGGr)   z1LINE: refusing to serve outside allowed roots: %si  	forbiddenr   zapplication/octet-streamr  )r   r  
match_infor  r   r  ru   r  r   existsis_filehermes_constantsrG  r6  rY   homejoinpathtempfile
gettempdirr;  r   r  	mimetypes
guess_typer   FileResponse)r   r  r  r  r   r2  r  r  rG  hermes_homeallowed_rootscontent_typer  r;  s                @r'   r  zLineAdapter._handle_media  sR      	 "7+"&&u-- 	><<s<=== %	:9;;##""5$///<<s<888I{{}} 	>DLLNN 	><<s<===	D888888001199;;KK 	D 	D 	D)++..y99AACCKKK	D $&&''//11LL  ""

 <<>>GGGGGGGGG 	>NNNPXYYY<<s<===#.s4yy99a#\%O5OP   
 
 	
s   '/D AEE
image_pathcaptionc                ,  K   t          |          }|                                r|                                st          dd|           S |                                j        t          k    rt          dd          S | j        st          dd          S | j        s| j	        dk    rt          dd          S | 
                    t          |                                                    }|                     ||j                  }|                                                    d          st          dd	|           S t#          |          g}|r"|                    t'          |                     |                     ||           d {V S )
NFzimage file not found: r  zimage exceeds 10 MB LINE limitr  r]  z\LINE_PUBLIC_URL must be set to send images (LINE only accepts publicly reachable HTTPS URLs)r?  zLINE image URL must be HTTPS: )r   rM  rN  r   statst_sizeLINE_IMAGE_MAX_BYTESr  r}  rw  r<  r   r6  rD  rG  rP  r   r  rA   r  _send_messages)	r   ro   rZ  r[  r  r  r  r
  msgss	            r'   send_image_filezLineAdapter.send_image_file  s      J{{}} 	ZDLLNN 	Ze3XJ3X3XYYYY99;;!555e3STTTT| 	Qe3OPPPP# 	(9Y(F(FD    $$S%8%899ooeTY//yy{{%%j11 	[e3YTW3Y3YZZZZ&4S&9&9%: 	0KKg..///(($777777777r)   r   
audio_pathr  r9   c                l  K   t          |          }|                                r|                                st          dd|           S |                                j        t          k    rt          dd          S | j        st          dd          S | j        s| j	        dk    rt          dd          S | 
                    t          |                                                    }|                     ||j                  }|                     |t!          ||          g           d {V S )NFzaudio file not found: r  zaudio exceeds 200 MB LINE limitr  r]  z)LINE_PUBLIC_URL must be set to send audio)r   rM  rN  r   r]  r^  LINE_AV_MAX_BYTESr  r}  rw  r<  r   r6  rD  rG  r`  r  )r   ro   rc  r  r  r  r  r
  s           r'   
send_voicezLineAdapter.send_voice8  s@      J{{}} 	ZDLLNN 	Ze3XJ3X3XYYYY99;;!222e3TUUUU| 	Qe3OPPPP# 	(9Y(F(FA   
 $$S%8%899ooeTY//((>#{3S3S2TUUUUUUUUUr)   
video_pathpreview_pathc                  K   t          |          }|                                r|                                st          dd|           S |                                j        t          k    rt          dd          S | j        st          dd          S | j        s| j	        dk    rt          dd          S |rwt          |                                          rV| 
                    t          t          |                                                              }t          |          j        }nt          j        dd	          }	 |                    t"                     |                                 |                                 | 
                    |j        d
          }d}n:# t(          $ r- 	 t+          j        |j                   n# t.          $ r Y nw xY w w xY w| 
                    t          |                                                    }	|                     |	|j                  }
|                     ||          }|                     |t5          |
|          g           d {V S )NFzvideo file not found: r  zvideo exceeds 200 MB LINE limitr  r]  z)LINE_PUBLIC_URL must be set to send videoz.png)suffixdeleteTr1  zpreview.png)r   rM  rN  r   r]  r^  re  r  r}  rw  r<  r   r6  rG  rR  NamedTemporaryFilewrite_FALLBACK_PNG_PREVIEWflushcloserY   rN  r  r  rD  r`  r"  )r   ro   rg  rh  r  r  preview_tokenpreview_filenametmpvideo_token	video_urlr  s               r'   
send_videozLineAdapter.send_videoP  s~      J{{}} 	ZDLLNN 	Ze3XJ3X3XYYYY99;;!222e3TUUUU| 	Qe3OPPPP# 	(9Y(F(FA     	D..6688 	 00T,5G5G5O5O5Q5Q1R1RSSM#L116-VEJJJC		/000				 $ 4 4SXt 4 L L#0     Ich''''   D **3t||~~+>+>??OOK;;	oom5EFF((>)[3Y3Y2Z[[[[[[[[[s1   A F- -
G$8GG$
GG$GG$r   r   c                ,  K   | j         st          dd          S |st          dd          S |dt                   }|t          d         }|                     |          \  }}|r	 | j                             ||           d{V  n# t
          $ r}t                              d|           	 | j                             ||           d{V  n:# t
          $ r-}t          dt          |                    cY d}~cY d}~S d}~ww xY wY d}~n`d}~ww xY w	 | j                             ||           d{V  n5# t
          $ r(}t          dt          |                    cY d}~S d}~ww xY w|r|dt                   }	|t          d         }	 | j                             ||	           d{V  nP# t
          $ rC}t          
                    d|           t          dt          |                    cY d}~S d}~ww xY w|t          dd          S )	z6Send already-built message objects, batched at 5/call.Fr  r  TNr  r  z)LINE: push for follow-up batch failed: %s)r  r   r@   r  r   rY   r   r  r   r   r  )
r   ro   r   first_batchrestr  r  r  r   batchs
             r'   r`  zLineAdapter._send_messages}  s      | 	Qe3OPPPP 	=dt<<<<: ::;2334 !55g>>z 	AFl((<<<<<<<<<< F F FSUXYYYF,++G[AAAAAAAAAA  F F F%e3t99EEEEEEEEEEEEEEF BAAAAFAl''========== A A A!%s3xx@@@@@@@@@A  	A4445E2334DAl''7777777777 A A AJCPPP!%s3xx@@@@@@@@@A  	A $48888s   '!B	 	
DD/!CD
DD8D9D=DDDD!D; ;
E-E("E-(E-!F3 3
H =8G;5H ;H r   rM   r(  )r   r   )r  r   r   r|   )r  r   r  r   r   r   )NN)
ro   r   r6  r   r  r   r  r  r   r   )ro   r   r6  r   r
  rM   r   r   )ro   r   r   r  r~   )ro   r   r   r|   )ro   r   r   r   )r6  r   r   r   )r.  r   ro   r   r   r|   )r2  r   r  rM   r   r   )r  r   r=  r   r   r   )
ro   r   rZ  r   r[  r   r  r  r   r   )r   N)
ro   r   rc  r   r  r9   r  r  r   r   )
ro   r   rg  r   rh  r   r  r  r   r   )ro   r   r   r   r   r   )rc   rd   re   r   r   r  r  r  r  r  r  r  r  r  r  r  r  r  r!  r)  r0  r<  rD  r  rb  rf  rv  r`  __classcell__)r  s   @r'   rS  rS  p  s       --
]3 ]3 ]3 ]3 ]3FO O O Ob "  "  "  "LG G G G3 3 3 3>!E !E !E !EF>- >- >- >-@9 9 9 9v   8 #'-1P P P P P2= = = =>   0 0 0 0 0

: 
: 
: 
:7 7 7 73 3 3 3 3 3j> > > > > > BG      ,H H H H-
 -
 -
 -
f "&-18 8 8 8 8D  -1V V V V V8 '+-1+\ +\ +\ +\ +\Z)9 )9 )9 )9 )9 )9 )9 )9r)   rS  childr   parentc                >   	 |                                                      |                                           S # t          t          f$ rP 	 |                                                      |                                            Y dS # t          $ r Y Y dS w xY ww xY w)uv   Backport for Path.is_relative_to (Python 3.9+) — defensive against
    cwd-resolution differences across CI runners.TF)r6  is_relative_toAttributeErrorr{  relative_to)r}  r~  s     r'   rI  rI    s    }}--fnn.>.>???J'   	MMOO''(8(899944 	 	 	555		s'   8; B9B		
BBBBc                     t          j        d          sdS t          j        d          sdS 	 ddl} n# t          $ r Y dS w xY wdS )z8Plugin gate: require credentials AND aiohttp at runtime.rY  FrZ  r   NT)rN  rO  r   r  )r   s    r'   check_requirementsr    si    9011 u9*++ u   uu4s   3 
A Ac                   t          | di           pi }t          t          j        d          p|                    d                    }t          t          j        d          p|                    d                    }|o|S )NrX  rY  r   rZ  rL   )rv  rM   rN  rO  r   )rV  rX  	has_token
has_secrets       r'   validate_configr    s    FGR((.BE
	-..S%))<R2S2S I 
	'((GEII6F,G,G J ##r)   c                     t          |           S )zESurface in ``hermes status`` even before the adapter is instantiated.)r  )rV  s    r'   is_connectedr    s    6"""r)   r  c                    t          j        d          rt          j        d          sdS i } t          j        d          r4	 t          t           j        d                   | d<   n# t          $ r Y nw xY wt          j        d          rt           j        d         | d<   t          j        d          rt           j        d         | d	<   t          j        d
          rt           j        d
         | d<   | pi S )zAuto-seed PlatformConfig.extra from env-only setups.

    Lets ``hermes status`` reflect a LINE configuration that lives entirely
    in ``.env`` without a ``platforms.line`` block in ``config.yaml``.
    Mirrors the IRC plugin's pattern.
    rY  rZ  Nr^  r_  r[  r\  ra  rb  LINE_HOME_CHANNELhome_channel)rN  rO  r9   environr{  )seededs    r'   _env_enablementr    s     I122 ryAV7W7W tF	y 	 K!899F6NN 	 	 	D		y 1K0v	y"## =!z*;<|	y$%% A!#,?!@~<Rs   "A% %
A21A2)	thread_idmedia_filesforce_documentro   r   r  r  Optional[List[str]]r  c               B  K   t          | di           pi }t          j        d          p|                    dd          }|r|sddiS t	          |pd          }t          |          pdg}	d |	D             dt                   }
|rB|
                    t          d	t          |           d
                     |
dt                   }
t          |          }	 |                    ||
           d{V  dddS # t          $ r}dt          |          icY d}~S d}~ww xY w)u  Out-of-process push delivery for cron jobs running detached from the gateway.

    Without this hook ``deliver=line`` cron jobs fail with ``no live adapter``
    when cron runs as its own process. We always Push (reply tokens require
    an inbound webhook event we don't have in this path).

    ``thread_id`` is accepted for signature parity but ignored — LINE has
    no native thread primitive on the channel-side API. ``media_files``
    likewise: cron-side media delivery requires a publicly-reachable URL,
    which the standalone path can't construct without binding the webhook
    server, so we send a text reference instead.
    rX  rY  r   r.   rb   z.LINE standalone send: missing token or chat_idc                ,    g | ]}t          |          S rj   r  r  s     r'   r  z$_standalone_send.<locals>.<listcomp>  s     111Qa  111r)   Nr  z4 attachment(s) generated; not deliverable from cron]Tr  )rv  rN  rO  r   r7   rH   r@   rA   r  r?   r   r   rY   r   )pconfigro   r   r  r  r  rX  r  plainrD   r   clientr  s                r'   _standalone_sendr    sw     * GWb))/RE
	-.. 	199+R00 
  K KIJJ*7=b99EE""*rdF11&1112M3M2MNH 9&p#k*:*:&p&p&pqqrrr7778F#kk'8,,,,,,,,,t444 # # #S"""""""#s    C9 9
DDDDr|   c                    t                       t          d           t          d           t          d           t          d           t                       	 ddlmm n # t          $ r t          d           Y dS w xY wd	d
dfd}  | ddd
            | ddd
            | dd            | dd           t          d           dS )zMinimal stdin wizard for ``hermes setup line``.

    Mirrors the irc/teams style: prompts for the two required vars, plus
    one optional public URL. Writes to ``~/.hermes/.env`` via ``hermes_cli.config``.
    zLINE Messaging API setupz------------------------zFCreate a Messaging API channel at https://developers.line.biz/console/zthen copy the values below.r   )get_env_varset_env_varzKhermes_cli.config not available; set LINE_* vars manually in ~/.hermes/.envNF)secretvarr   promptr  rM   r   r|   c               J   t                    r |           nd }|rdnd}	 |rdd l}|                    | | d          }n&t          | | d                                          }n&# t          t
          f$ r t                       Y d S w xY w|r | |           d S d S )Nz [keep current]r.   r   r   )callablegetpassinputr@  EOFErrorKeyboardInterruptprint)	r  r  r  existingrj  r  r=  r  r  s	          r'   _promptz"interactive_setup.<locals>._prompt/  s    '/'<'<F;;s###$&.6""B	 =6(=6(=(=(=>>333344::<<+, 	 	 	GGGFF	  	$KU#####	$ 	$s   AA- -BBrY  zChannel access tokenTrZ  zChannel secretra  zDPublic HTTPS base URL (optional, e.g. https://my-tunnel.example.com)rf  z.Allowed user IDs (comma-separated; blank=skip)ziDone. Set the webhook URL in the LINE console to <your-public-url>/line/webhook and enable 'Use webhook'.)r  r   r  r   r  rM   r   r|   )r  hermes_cli.configr  r  r  )r  r  r  s    @@r'   interactive_setupr    sZ    
GGG	
$%%%	
$%%%	
RSSS	
'(((	GGG>>>>>>>>>   [\\\ :? $ $ $ $ $ $ $ $ $ G')?MMMMG!#3DAAAAGefffG "RSSS	 E F F F F Fs   A% %BBc                    |                      ddd t          t          t          ddgdt          t
          dt          dd	t          d
ddd           dS )uE   Plugin entry point — called by the Hermes plugin system at startup.rU  LINEc                     t          |           S r~   )rS  )cfgs    r'   r-   zregister.<locals>.<lambda>K  s    K$4$4 r)   rY  rZ  zpip install aiohttpr  rf  rd  u   💚FTu  You are chatting via LINE Messaging API. LINE does NOT render Markdown — text bubbles show ** and # literally. Bare URLs are auto-linked, but \[label\](url) syntax is not. Each text bubble is capped at 5000 characters and at most 5 bubbles are sent per reply, so keep responses concise. Image/audio/video sending requires LINE_PUBLIC_URL configured to a publicly reachable HTTPS host. Slow responses surface a 'Get answer' button the user taps to fetch the reply via a fresh free token.)rG  r-  adapter_factorycheck_fnr  r  required_envinstall_hintsetup_fnenv_enablement_fncron_deliver_env_varstandalone_sender_fnallowed_users_envallow_all_envmax_message_lengthemojipii_safeallow_update_commandplatform_hintN)register_platformr  r  r  r  r  r  LINE_SAFE_BUBBLE_CHARS)ctxs    r'   registerr  F  sm    44#'!13HI*")0-.,1!9+      r)   )r   r   r   r   )r   r   r8   r9   r   r:   )rI   rJ   rK   r   rL   r   r   rM   )r   r   r   r   )r   r   r   rM   r   r   r   r   r   r   r   rM   )r   r   r   r   r~   )r  r   r  r   r   r   r   )r
  r   r  r9   r   r   )r
  r   r  r   r   r   )r   r   r#  r   r   r   r   r   )r6  r   r   rM   )r=  r   r   r   )F)rG  r   rH  rM   r   rM   )r}  r   r~  r   r   rM   r{  )r   r  )ro   r   r   r   r  r   r  r  r  rM   r   r   r(  )or   
__future__r   r  rV   enumrS   rP   r   loggingrT  rN  rer7  rR  ru   r   dataclassesr   r   pathlibr   typingr   r   r	   r
   r   r   r   r   urllib.parser   rA  	getLoggerrc   r   gateway.platforms.baser   r   r   r   r   gateway.configr   gateway.sessionr   r   r   r   r  r  r  r  r@   r  r  rx  r|  r  r~  r  r  r  r  r  r_  re  rJ   fromhexrn  compiler2   r3   r4   r1   DOTALLr/   	MULTILINEr5   r6   r7   rH   r\   Enumr^   rl   rw   r   r   r   r   r  r  r  r"  r4  r5  rt   r<  rF  rQ  rS  rI  r  r  r  r  r  r  r  rj   r)   r'   <module>r     s  : : :x # " " " " "            				 				     ( ( ( ( ( ( ( (       M M M M M M M M M M M M M M M M M M M M + + + + + +		8	$	$              $ # # # # # ) ) ) ) ) ) <9B U 5     !  #  & )  #' I  $ . C   ' % 
    bj>??bj)**bj?@@RZ-- BJ?KK M2<88
,bl;;# # # #L 0F ' ' ' ' '\4 4 4 42    DI    9 9 9 9 9 9 9 9C C C C C C C CT       0   (   8S S S S S S S St* * * *              J,     G G G G> > > >; ; ; ; ;v9 v9 v9 v9 v9% v9 v9 v9r
 
 
 
"
 
 
 
$ $ $ $# # # #
   :  $'+ *# *# *# *# *# *#Z'F 'F 'F 'FT           r)   