
    o;i)                   2   d dl mZ 	 d dlZd dlZd dlZd dlZd dlZd dlZd dlZd dl	Z	d dl
mZ d dlmZmZmZmZmZmZ  ej        e          Zh dZh dZ	 d dlZd dlmZmZ d dlmZ d	Zn# e$ r d
ZdZeZeZdZY nw xY wd dl Z d dl!m"Z# e j$        %                    d  e& e#e'          (                                j)        d                              d dl*m+Z+m,Z, d dl-Z-d dl.m/Z/m0Z0 d dl1m2Z2m3Z3m4Z4m5Z5m6Z6m7Z7m8Z8m9Z9m:Z:m;Z;m<Z< d dl=m>Z> d(dZ?d)dZ@d ZA G d d          ZB G d de2          ZCd*dZDerb G d  d!ejE        jF                  ZG G d" d#ejE        jF                  ZH G d$ d%ejE        jF                  ZI G d& d'ejE        jF                  ZJdS dS )+    )annotationsN)defaultdict)CallableDictListOptionalAnyTuple>   `'    <     >   offbulksafe)MessageIntents)commandsTF)Path   )PlatformPlatformConfig)MessageDeduplicatorThreadParticipationTracker)BasePlatformAdapterMessageEventMessageTypeProcessingOutcome
SendResultcache_image_from_urlcache_image_from_bytescache_audio_from_urlcache_audio_from_bytescache_document_from_bytesSUPPORTED_DOCUMENT_TYPES)is_safe_urlentrystrreturnc                X   |                                  } |                     d          r=|                     d          r(|                     d                              d          } |                                                     d          r
| dd         } |                                  S )a  Strip common prefixes from a Discord user ID or username entry.

    Users sometimes paste IDs with prefixes like ``user:123``, ``<@123>``,
    or ``<@!123>`` from Discord's UI or other tools.  This normalises the
    entry to just the bare ID or username.
    <@><@!zuser:   N)strip
startswithendswithlstriprstriplower)r'   s    >/home/ubuntu/.hermes/hermes-agent/gateway/platforms/discord.py_clean_discord_idr6   @   s     KKMME 0%.."5"5 0U##**3//{{}}(( abb	;;==    boolc                     t           S )z,Check if Discord dependencies are available.)DISCORD_AVAILABLE r7   r5   check_discord_requirementsr<   Q   s    r7   c            	         t           sdS dd} t          j         | dd	           | d
d	           | dd           | dd                    S )u|  Build Discord ``AllowedMentions`` with safe defaults, overridable via env.

    Discord bots default to parsing ``@everyone``, ``@here``, role pings, and
    user pings when ``allowed_mentions`` is unset on the client — any LLM
    output or echoed user content that contains ``@everyone`` would therefore
    ping the whole server. We explicitly deny ``@everyone`` and role pings
    by default and keep user / replied-user pings enabled so normal
    conversation still works.

    Override via environment variables (or ``discord.allow_mentions.*`` in
    config.yaml):

        DISCORD_ALLOW_MENTION_EVERYONE      default false  — @everyone + @here
        DISCORD_ALLOW_MENTION_ROLES         default false  — @role pings
        DISCORD_ALLOW_MENTION_USERS         default true   — @user pings
        DISCORD_ALLOW_MENTION_REPLIED_USER  default true   — reply-ping author
    Nnamer(   defaultr8   r)   c                    t          j        | d                                                                          }|s|S |dv S )N true1yeson)osgetenvr/   r4   )r>   r?   raws      r5   _bz#_build_allowed_mentions.<locals>._bk   sD    ib!!''))//11 	N000r7   DISCORD_ALLOW_MENTION_EVERYONEFDISCORD_ALLOW_MENTION_ROLESDISCORD_ALLOW_MENTION_USERST"DISCORD_ALLOW_MENTION_REPLIED_USER)everyonerolesusersreplied_user)r>   r(   r?   r8   r)   r8   )r:   discordAllowedMentions)rJ   s    r5   _build_allowed_mentionsrU   V   s    $  t1 1 1 1 "4e<<b.66b.55R<dCC	   r7   c                      e Zd ZdZdZdZdZdZd d!d	Zd
 Z	d Z
d Zd Zd"dZd Zd#dZd$dZd%dZe	 d&d'd            ZdS )(VoiceReceivera7  Captures and decodes voice audio from a Discord voice channel.

    Attaches to a VoiceClient's socket listener, decrypts RTP packets
    (NaCl transport + DAVE E2EE), decodes Opus to PCM, and buffers
    per-user audio.  A polling loop detects silence and delivers
    completed utterances via a callback.
    g      ?g      ?逻  r   Nallowed_user_idssetc                   || _         |pt                      | _        d| _        d | _        d | _        d| _        i | _        t          j	                    | _
        t          t                    | _        i | _        i | _        d| _        d| _        d S )NFr   )_vcrZ   _allowed_user_ids_running_secret_key_dave_session	_bot_ssrc_ssrc_to_user	threadingLock_lockr   	bytearray_buffers_last_packet_time	_decoders_paused_packet_debug_count)selfvoice_clientrY   s      r5   __init__zVoiceReceiver.__init__   s    !1!:SUU -1! .0^%%
 /:).D.D35 -/  $%   r7   c                ,   | j         j        }t          |j                  | _        |j        | _        |j        | _        | 	                    |           |
                    | j                   d| _        t                              d| j                   dS )z"Start listening for voice packets.Tz#VoiceReceiver started (bot_ssrc=%d)N)r\   _connectionbytes
secret_keyr_   dave_sessionr`   ssrcra   _install_speaking_hookadd_socket_listener
_on_packetr^   loggerinfo)rl   conns     r5   startzVoiceReceiver.start   s    x# 11!.##D)))  11194>JJJJJr7   c                   d| _         	 | j        j                            | j                   n# t
          $ r Y nw xY w| j        5  | j                                         | j	                                         | j
                                         | j                                         ddd           n# 1 swxY w Y   t                              d           dS )zStop listening and clean up.FNzVoiceReceiver stopped)r^   r\   rp   remove_socket_listenerrw   	Exceptionre   rg   clearrh   ri   rb   rx   ry   rl   s    r5   stopzVoiceReceiver.stop   s   	H 77HHHH 	 	 	D	Z 	' 	'M!!!"((***N  """$$&&&		' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	' 	'
 	+,,,,,s   $. 
;;A%B66B:=B:c                    d| _         d S NTrj   r   s    r5   pausezVoiceReceiver.pause   s    r7   c                    d| _         d S )NFr   r   s    r5   resumezVoiceReceiver.resume   s    r7   rt   intuser_idc                Z    | j         5  || j        |<   d d d            d S # 1 swxY w Y   d S N)re   rb   )rl   rt   r   s      r5   map_ssrczVoiceReceiver.map_ssrc   s{    Z 	/ 	/'.Dt$	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/s    $$c                0   |j         | fd}||_         	 ddlm} t          |d          r1|j        |ur*||j        _        t                              d           dS dS dS # t          $ r&}t          	                    d|           Y d}~dS d}~ww xY w)aK  Wrap the voice websocket hook to capture SPEAKING events (op 5).

        VoiceConnectionState stores the hook as ``conn.hook`` (public attr).
        It is passed to DiscordVoiceWebSocket on each (re)connect, so we
        must wrap it on the VoiceConnectionState level AND on the current
        live websocket instance.
        c                  K   t          |t                    r|                    d          dk    r|                    di           }|                    d          }|                    d          }|rN|rLt                              d||                               t          |          t          |                     r | |           d {V  d S d S )Nopr.   drt   r   z"SPEAKING event: ssrc=%d -> user=%s)
isinstancedictgetrx   ry   r   r   )wsmsgdatart   r   original_hookreceiver_selfs        r5   wrapped_hookz:VoiceReceiver._install_speaking_hook.<locals>.wrapped_hook   s      #t$$ D!););wwsB''xx''((9-- DG DKK DdGTTT!**3t99c'llCCC -#mB,,,,,,,,,,,- -r7   r   )MISSINGr   z)Speaking hook installed on live websocketz%Could not install hook on live ws: %sN)
hookdiscord.utilsr   hasattrr   _hookrx   ry   r~   warning)rl   rz   r   r   er   r   s        @@r5   ru   z$VoiceReceiver._install_speaking_hook   s     			- 		- 		- 		- 		- 		- !		G------tT"" Itwg'='= ,GHHHHHI I'='=  	G 	G 	GNNBAFFFFFFFFF	Gs   AA% %
B/BBr   rq   c           	     
   | j         r| j        rd S | xj        dz  c_        | j        dk    rXt                              dt          |          t          |          dk    r|d d                                         nd           t          |          dk     rd S |d         dz	  d	k    s|d         d
z  dk    r5| j        dk    r(t                              d|d         |d                    d S |d         }t          j        d|d          \  }}}}}|| j	        k    rd S |dz  }t          |dz            }t          |dz            }	dd|z  z   |rdndz   }
t          |          |
dz   k     rd S d}|r,dd|z  z   }t          j        d||d	z             d         }|dz  }| j        dk    rY| j        5  | j                            |d          }d d d            n# 1 swxY w Y   t                              d||||
|           t          |d |
                   }||
d          }t          |          dk     rd S t          d          }|dd          |d d<   t          |d d                   }	 dd l}|j                            | j                  }|                    ||t          |                    }nM# t*          $ r@}| j        dk    r*t                              d||
t          |                     Y d }~d S d }~ww xY w|rt          |          |k    r
||d          }|	r|s(| j        dk    rt                              d|           d S |d         }|dk    s|t          |          k    r7| j        dk    r*t                              d|t          |          |           d S |d |          }|sd S | j        r| j        5  | j                            |d          }d d d            n# 1 swxY w Y   |r	 dd l}| j                            ||j        j        |          }nU# t*          $ rH}dt7          |          vr-| j        dk    rt                              d||           Y d }~d S Y d }~nd }~ww xY w	 || j        vr&t:          j                                        | j        |<   | j        |                              |          }| j        5  | j!        |         "                    |           tG          j$                    | j%        |<   d d d            d S # 1 swxY w Y   d S # t*          $ r'}t                              d||           Y d }~d S d }~ww xY w)N   r.   z&Raw UDP packet: len=%d, first_bytes=%s   short   r      r      x   z*Skipped non-RTP: byte0=0x%02x byte1=0x%02xz>BBHII          z>H
   unknownz9RTP packet: ssrc=%d, seq=%d, user=%s, hdr=%d, ext_data=%d   z(NaCl decrypt failed: %s (hdr=%d, enc=%d)z,RTP padding bit set but no payload (ssrc=%d)z;Invalid RTP padding length %d for payload size %d (ssrc=%d)Unencryptedz#DAVE decrypt failed for ssrc=%d: %sz!Opus decode error for SSRC %s: %s)&r^   rj   rk   rx   debuglenhexstructunpack_fromra   r8   re   rb   r   rq   rf   nacl.secretsecretAeadr_   decryptr~   r   r`   davey	MediaTypeaudior(   ri   rS   opusDecoderdecoderg   extendtime	monotonicrh   )rl   r   
first_byte_seq	timestamprt   cchas_extensionhas_paddingheader_sizeext_data_lenext_preamble_offset	ext_words
known_userheaderpayload_with_noncenonce	encryptednaclbox	decryptedr   pad_lenr   r   pcms                              r5   rw   zVoiceReceiver._on_packet   s   } 	 	F 	  A%  #q((LL8D		SYY!^^48<<>>>  
 t99r>>F
 GqLQ47T>d":":'1,,I4PQ7TXYZT[\\\F!W
%+%7$%J%J"1c9d 4>!!F $Z$.//:,--AFmM'@qqqAt99{Q&&F  	)"$B-*47JQ7NOOPQRI$q=L#r)) E E!/33D)DD
E E E E E E E E E E E E E E ELLKc:{L  
 tL[L)**!+,,/ !""Q&&F"&rss+bqb	,SbS122		+""4#344CIvuU||DDII 	 	 	'2--I1k[^_h[i[ijjjFFFFF	  	1C	NN\99!,--0I  	 +r11NNF   mG!||wY77+r11NNUY   !)G8),I   	 : :,00q99: : : : : : : : : : : : : : : 
 LLL $ 2 : :!6	! !II !   $CFF223r99"NN+PRVXYZZZ 32222		4>))'.|';';'='=t$.&--i88C @ @d#**3////3~/?/?&t,@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @  	 	 	LL<dAFFFFFFFF	s   /GGG-AJ5 5
K??5K::K?O**O.1O.8*P# #
Q5-8Q00Q59AT% <TT% TT% T T% %
U/UUr)   c                H   	 | j         j        }|sdS | j         j        r| j         j        j        nd| j        fd|j        D             }t          |          dk    r0|d         }|| j        |<   t          	                    d||           |S n# t          $ r Y nw xY wdS )zTry to infer user_id for an unmapped SSRC.

        When the bot rejoins a voice channel, Discord may not resend
        SPEAKING events for users already speaking.  If exactly one
        allowed user is in the channel, map the SSRC to them.
        r   c                d    g | ],}|j         k    rt          |j                   v %|j         -S r;   )idr(   ).0mallowedbot_ids     r5   
<listcomp>z6VoiceReceiver._infer_user_for_ssrc.<locals>.<listcomp>  sF       46>>7>c!$ii76J6J 6J6J6Jr7   r   z4Auto-mapped ssrc=%d -> user=%d (sole allowed member))r\   channeluserr   r]   membersr   rb   rx   ry   r~   )rl   rt   r   
candidatesuidr   r   s        @@r5   _infer_user_for_ssrcz"VoiceReceiver._infer_user_for_ssrc}  s    	h&G q)-=TX]%%AF,G    %o  J :!## m+."4(RTXZ]^^^
	 $
  	 	 	D	qs   B A<B 
BBlistc                N   t          j                    }g }| j        5  t          | j                  }t          | j                                                  }|D ]1}| j        	                    ||          }||z
  }| j        |         }t          |          | j        | j        z  dz  z  }	|| j        k    r|	| j        k    r|	                    |d          }
|
s|                     |          }
|
r$|                    |
t#          |          f           t%                      | j        |<   | j                            |d           || j        dz  k    r6| j                            |d           | j                            |d           3	 ddd           n# 1 swxY w Y   |S )z=Return list of (user_id, pcm_bytes) for completed utterances.r   r   N)r   r   re   r   rb   r   rg   keysrh   r   r   SAMPLE_RATECHANNELSSILENCE_THRESHOLDMIN_SPEECH_DURATIONr   appendrq   rf   pop)rl   now	completedssrc_user_map	ssrc_listrt   	last_timesilence_durationbufbuf_durationr   s              r5   check_silencezVoiceReceiver.check_silence  s   n	Z 	; 	; !344MT]//1122I! ; ; 266tSAA	#&? mD)"3xx4+;dm+Ka+OP#t'===,RVRjBjBj+//a88G" B #'";";D"A"A @!(('5::)>???*3++DM$'*..tT::::%)?!)CCCM%%dD111*..tT:::);		; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	; 	;4 s   E0FF!Fpcm_dataoutput_pathr(   src_ratesrc_channelsc                   t          j        dd          5 }|                    |            |j        }ddd           n# 1 swxY w Y   	 t	          j        ddddd	d
dt          |          dt          |          d|dddd|gdd           	 t          j        |           dS # t          $ r Y dS w xY w# 	 t          j        |           w # t          $ r Y w w xY wxY w)z-Convert raw PCM to 16kHz mono WAV via ffmpeg.z.pcmF)suffixdeleteNffmpegz-yz	-loglevelerrorz-fs16lez-arz-acz-i16000rD   Tr   )checktimeout)
tempfileNamedTemporaryFilewriter>   
subprocessrunr(   rG   unlinkOSError)r   r   r   r   fpcm_paths         r5   
pcm_to_wavzVoiceReceiver.pcm_to_wav  sa    (uEEE 	GGHvH	 	 	 	 	 	 	 	 	 	 	 	 	 	 		NdK'3x==3|,,(73	    	(#####   	(####   sT   A  AAAB6 B% %
B32B36C8CC
CCCCr   )rY   rZ   )rt   r   r   r   )r   rq   )rt   r   r)   r   )r)   r   )rX   r   )r   rq   r   r(   r   r   r   r   )__name__
__module____qualname____doc__r   r   r   r   rn   r{   r   r   r   r   ru   rw   r   r   staticmethodr  r;   r7   r5   rW   rW   y   s         KH% % % % %>
K 
K 
K- - -    / / / /G G GJD D D DT   4   J >?    \  r7   rW   c                      e Zd ZdZdZdZdZd fdZdd
ZddZ	ddZ
ddZddZedd            ZddZddZddZddZddZdd Zdd!Zdd$Zdd'Z	 	 ddd/Zdd1Zd(d2d(d(d3dd8Zd9d:dd=Z	 	 dddAZ	 	 dd̈ fdGZddIZ	 	 	 ddψ fdJZddKZ ddNZ!dOZ"ddPZ#ddRZ$ddSZ%ddTZ&ddUZ'ddVZ(ddWZ)dXZ*ddYZ+dd\Z,ddd]Z-ddaZ.ddcZ/ddeZ0ddgZ1	 	 	 ddވ fdiZ2	 	 	 dd߈ fdkZ3	 	 	 dd fdmZ4	 	 	 dd fdoZ5	 	 	 	 dd fdpZ6dddqZ7ddrZ8ddsZ9ddtZ:dduZ;	 dddyZ<ddzZ=dd{Z>dd|Z?dd}Z@ddZAddZB	 	 dddZCddZDdddZEdddZFddZGddZHddZIddZJd2ddddZKddZL	 	 dddZM	 dddZN	 	 	 dddZO	 dddZPddZQddZRdddZSddZTddZUd dZVd dZWddZXddZYddZZddZ[ddZ\ xZ]S (  DiscordAdapteraD  
    Discord bot adapter.

    Handles:
    - Receiving messages from servers and DMs
    - Sending responses with Discord markdown
    - Thread support
    - Native slash commands (/ask, /reset, /status, /stop)
    - Button-based exec approvals
    - Auto-threading for long conversations
    - Reaction-based feedback
    i  il  ,  configr   c                    t                                          |t          j                   d | _        t          j                    | _        t                      | _	        t                      | _
        d | _        i | _        i | _        t          t          j        dd                    | _        t          t          j        dd                    | _        i | _        i | _        i | _        i | _        i | _        i | _        i | _        d | _        d | _        t9          d          | _        i | _        d | _        d | _         tC                      | _"        tG          |dd          pd| _$        | j%        j&        '                    dd	          | _(        d S )
N'HERMES_DISCORD_TEXT_BATCH_DELAY_SECONDSz0.6-HERMES_DISCORD_TEXT_BATCH_SPLIT_DELAY_SECONDSz2.0rS   reply_to_modefirstslash_commandsT))superrn   r   DISCORD_clientasyncioEvent_ready_eventrZ   r]   _allowed_role_idsgateway_runner_voice_clients_voice_locksfloatrG   rH   _text_batch_delay_seconds_text_batch_split_delay_seconds_pending_text_batches_pending_text_batch_tasks_voice_text_channels_voice_sources_voice_timeout_tasks_voice_receivers_voice_listen_tasks_voice_input_callback_on_voice_disconnectr   _threads_typing_tasks	_bot_task_post_connect_taskr   _dedupgetattr_reply_to_moder  extrar   _slash_commands)rl   r  	__class__s     r5   rn   zDiscordAdapter.__init__  sb   !1222/3#MOO&)ee&)ee".057).ry9bdi/j/j)k)k&/4RY?npu5v5v/w/w,>@"BD&46!9;=?!:<<> 9="8<! 39== 7915:> *++ $+6?G#L#L#WPW%)[%6%:%:;KT%R%Rr7   r)   r8   c                	   K   t           s"t                              d j                   dS t          j                                        sddl}|j        	                    d          }|s:d}t          j        dk    r(|D ]%}t          j                            |          r|} n&|rL	 t          j                            |           n+# t           $ r t                              d|           Y nw xY wt          j                                        st                              d	            j        j        s"t                              d
 j                   dS 	                      d j        j        d          sdS t          j        dd          }|r$d |                    d          D              _        t          j        dd          }|r$d |                    d          D              _        t3          j                    }d|_        d|_        d|_        t=          d  j        D                       pt?           j                  |_         d|_!        ddl"m#}m$}	  |d          }
|
r!t          %                    d j        |
            j&        	  j&        '                                s j&        (                                 d{V  n0# t           $ r# t          )                    d j                   Y nw xY wd _&         j*        +                                 n%# d _&         j*        +                                 w xY wtY          j-        d&d|t]                      d |	|
           _&          j&        j/        fd            } j&        j/        d' fd            } j&        j/        fd             } j0        r 1                                 te          j3         j&        4                     j        j                             _5        te          j6         j*        7                                d!"           d{V  d _8        dS # td          j9        $ r: t                              d# j        d$            :                                 Y dS t           $ rB}t                              d% j        |d$            :                                 Y d}~dS d}~ww xY w)(z.Connect to Discord and start receiving events.z:[%s] discord.py not installed. Run: pip install discord.pyFr   Nr   )z/opt/homebrew/lib/libopus.dylibz/usr/local/lib/libopus.dylibdarwinz)Opus codec found at %s but failed to loadu8   Opus codec not found — voice channel playback disabledz[%s] No bot token configuredzdiscord-bot-tokenzDiscord bot tokenDISCORD_ALLOWED_USERSrA   c                T    h | ]%}|                                 t          |          &S r;   )r/   r6   )r   r   s     r5   	<setcomp>z)DiscordAdapter.connect.<locals>.<setcomp>?  s>     * * */2yy{{*%c*** * *r7   ,DISCORD_ALLOWED_ROLESc                    h | ]I}|                                                                 (t          |                                           JS r;   )r/   isdigitr   )r   rids     r5   rC  z)DiscordAdapter.connect.<locals>.<setcomp>H  sR     * * *),yy{{**,,*		$$* * *r7   Tc              3  @   K   | ]}|                                  V  d S r   )rG  )r   r'   s     r5   	<genexpr>z)DiscordAdapter.connect.<locals>.<genexpr>Y  s-      LLE'LLLLLLr7   )resolve_proxy_urlproxy_kwargs_for_botDISCORD_PROXYplatform_env_varz [%s] Using proxy for Discord: %sz,[%s] Failed to close previous Discord client!)command_prefixintentsallowed_mentionsc                   K   t                               d j         j        j                                                     d {V   j                                          j        r2 j        	                                s j        
                                 t          j                                                    _        d S )Nz[%s] Connected as %s)rx   ry   r>   r!  r   _resolve_allowed_usernamesr$  rZ   r8  donecancelr"  create_task _run_post_connect_initialization)adapter_selfs   r5   on_readyz(DiscordAdapter.connect.<locals>.on_ready  s      2L4E|G[G`aaa #==?????????)--///2 =<;Z;_;_;a;a = 3::<<<292E AACC3 3///r7   messageDiscordMessagec                  K   j                                         sJ	 t          j        j                                         d           d {V  n# t          j        $ r Y nw xY wj                            t          | j	                            rd S | j
        j        j        k    rd S | j        t          j        j        t          j        j        fvrd S t%          | j
        dd          rit'          j        dd                                                                          }|dk    rd S |dk    r!j        j        rj        j        | j        vrd S n4                    t          | j
        j	                  | j
                  sd S t3          | j        t          j                  s| j        rxj        j        d uoj        j        | j        v }t9          fd| j        D                       }|r|sd S t'          j        d	d
                                          dv }|r|s|sd S                     |            d {V  d S )Ng      >@r  botFDISCORD_ALLOW_BOTSnonementionsc              3  F   K   | ]}|j         o|j        j        k    V  d S r   )r`  r!  r   )r   r   rl   s     r5   rJ  z=DiscordAdapter.connect.<locals>.on_message.<locals>.<genexpr>  sH       0 0 8!t|'8"80 0 0 0 0 0r7   DISCORD_IGNORE_NO_MENTIONrC   rC   rD   rE   )r$  is_setr"  wait_forwaitTimeoutErrorr9  is_duplicater(   r   authorr!  r   typerS   r   r?   replyr:  rG   rH   r4   r/   rc  _is_allowed_userr   r   	DMChannelany_handle_message)r\  
allow_bots_self_mentioned_other_bots_mentioned_ignore_no_mentionrZ  rl   s        r5   
on_messagez*DiscordAdapter.connect.<locals>.on_message  s     
 $07799 %.|/H/M/M/O/OY]^^^^^^^^^^^"/     &33C
OODD F >T\%666F <(;(CWEXE^'___F 7>5%88 !#+?!H!H!N!N!P!P!V!V!X!XJ!V++#z11#|0 #DL4EWM]4]4]"F
  00W^5F1G1GXX  "'/73DEE 'JZ )5 B L-1AA $ -0 0 0 0 0!(!10 0 0 - -)
 - _  *,3V* *egg!5*6& * / J_ **733333333333s   3A A$#A$c           	       K   t          j                                                  }|sdS | j        j        }||vrdS | j        j        k    rdS |j        du o|j        du}|j        duo|j        du }|j        duo|j        duo|j        |j        k    }|s|s|rft          	                    d| j
        | j        |rd|j        j        z   n,|rd|j        j        z   nd|j        j         d|j        j         |           dS dS )z&Track voice channel join/leave events.Nz"Voice state: %s (%d) %s (guild %d)zjoined zleft zmoved z -> )rZ   r'  r   guildr   r!  r   r   rx   ry   display_namer>   )	memberbeforeafterbot_guild_idsguild_idjoinedleftswitchedrZ  s	           r5   on_voice_state_updatez5DiscordAdapter.connect.<locals>.on_voice_state_update  s`      !$L$?$D$D$F$F G G$ F!<?=00F\1666F4/MEM4M~T1Kemt6KN$. 8T18%-7   	T 	X 	KK<+	:@ T	EM$666>B TWv~':::Sfn&9SSu}?QSS     	 	r7      r_  z.[%s] Timeout waiting for connection to Discordexc_infoz%[%s] Failed to connect to Discord: %sr;   )r\  r]  );r:   rx   r  r>   rS   r   	is_loadedctypes.utilutilfind_librarysysplatformrG   pathisfile	load_opusr~   r   r  token_acquire_platform_lockrH   splitr]   r%  r   r?   message_contentdm_messagesguild_messagesrq  r8   r   voice_statesgateway.platforms.baserK  rL  ry   r!  	is_closedcloser   r$  r   r   BotrU   eventr=  _register_slash_commandsr"  rX  r{   r7  rh  ri  r^   rj  _release_platform_lock)rl   ctypes	opus_path_homebrew_paths_hpallowed_env	roles_envrR  rK  rL  	proxy_urlr[  rw  r  r   rZ  s   `              @r5   connectzDiscordAdapter.connect  s       	LLUW[W`aaa5 |%%'' 	[0088I  	"# <8++. " "7>>#.. "(+I!E"  [[L**95555  [ [ [NN#NPYZZZZZ[<))++ [YZZZ{  	LL7CCC5X	../BDKDUWjkk u )$;R@@K * *6A6G6G6L6L* * *& 	"92>>I * *090D0D* * *& o''G&*G#"&G%)G"LLT5KLLLLL 0.// O $(G  WVVVVVVV))?KKKI V>	9UUU |'.<1133 3"l00222222222  \ \ \LL!OQUQZ[[[[[\ $(DL%++---- $(DL%++----#< "!8!:!:  '&y11	 DL  L \      \I4 I4 I4 I4 I4 I4  I4V \     B # 0--/// %01C1CDKDU1V1VWWDN "4#4#9#9#;#;RHHHHHHHHHH DM4# 	 	 	LLI49_cLddd'')))55 	 	 	LL@$)QY]L^^^'')))55555	so   )C	 	%C10C1!Q >DQ 8K L# *K?<L# >K??L# !Q #"MDQ AS%	S%#7S  S%Nonec                H  K   t          | j                                                  D ]W}	 |                     |           d{V  # t          $ r,}t
                              d| j        ||           Y d}~Pd}~ww xY w| j        r[	 | j        	                                 d{V  n:# t          $ r-}t
          
                    d| j        |d           Y d}~nd}~ww xY w| j        rV| j                                        s=| j                                         	 | j         d{V  n# t          j        $ r Y nw xY wd| _        d| _        | j                                         d| _        |                                  t
                              d| j                   dS )zDisconnect from Discord.Nz'[%s] Error leaving voice channel %s: %sz [%s] Error during disconnect: %sTr  Fz[%s] Disconnected)r   r'  r   leave_voice_channelr~   rx   r   r>   r!  r  r   r8  rV  rW  r"  CancelledErrorr^   r$  r   r  ry   )rl   r  r   s      r5   
disconnectzDiscordAdapter.disconnect  s      T0557788 	` 	`H`..x8888888888 ` ` `F	S[]^________` < 	``l((********** ` ` `A49aZ^________` " 	4+B+G+G+I+I 	#**,,,---------)    !!!"&##%%%'33333sA   A
A>"A99A>	B) )
C 3#CC D+ +D=<D=c                X  K   | j         sdS 	 |                                 }|dk    r"t                              d| j                   dS |dk    rht          j        | j         j                                        d           d{V }t                              d| j        t          |                     dS t          j        | 
                                d           d{V }t                              d	| j        |d
         |d         |d         |d         |d         |d                    dS # t
          j        $ r$ t                              d| j                   Y dS t
          j        $ r  t          $ r.}t                              d| j        |d           Y d}~dS d}~ww xY w)z<Finish non-critical startup work after Discord is connected.Nr   z5[%s] Skipping Discord slash command sync (policy=off)r   r  r_  z2[%s] Synced %d slash command(s) via bulk tree synciX  zf[%s] Safely reconciled %d slash command(s): unchanged=%d updated=%d recreated=%d created=%d deleted=%dtotal	unchangedupdated	recreatedcreateddeletedun   [%s] Slash command sync timed out — Discord rate-limit bucket may be saturated; will retry on next reconnectz"[%s] Slash command sync failed: %sTr  )r!   _get_discord_command_sync_policyrx   ry   r>   r"  rh  treesyncr   _safe_sync_slash_commandsrj  r   r  r~   )rl   sync_policysyncedsummaryr   s        r5   rY  z/DiscordAdapter._run_post_connect_initialization1  s     | 	F&	^??AAKe##SUYU^___f$$&/0A0F0F0H0HRTUUUUUUUUUPRVR[]`ag]h]hiii $,T-K-K-M-MWZ[[[[[[[[[GKKx	 $	"$	"	"	 	 	 	 	 # 	 	 	NNA	     
 % 	 	 	 	^ 	^ 	^NN?AX\N]]]]]]]]]	^s+   :D1 	A,D1 7A8D1 1/F)#F);#F$$F)r(   c                    t          t          j        dd          pd                                                                          }|t
          v r|S |r!t                              d| j        |           dS )NDISCORD_COMMAND_SYNC_POLICYr   rA   zC[%s] Invalid DISCORD_COMMAND_SYNC_POLICY=%r; falling back to 'safe')	r(   rG   rH   r/   r4   _DISCORD_COMMAND_SYNC_POLICIESrx   r   r>   )rl   rI   s     r5   r  z/DiscordAdapter._get_discord_command_sync_policy]  sz    ")96BBHbIIOOQQWWYY000J 	NNU	  
 vr7   payloadDict[str, Any]c                    |                     d          }|                     d          }t          |                     dd          pd          t          |                     dd          pd          t          |                     dd          pd                               |                     d                    t	          |                     d	d
                    t	          |                     dd                    |rt          d |D                       nd|rt          d |D                       nd fd|                     dg           pg D             d	S )z>Reduce command payloads to the semantic fields Hermes manages.contextsintegration_typesrm  r   r>   rA   descriptiondefault_member_permissionsdm_permissionTnsfwFc              3  4   K   | ]}t          |          V  d S r   r   r   cs     r5   rJ  zCDiscordAdapter._canonicalize_app_command_payload.<locals>.<genexpr>v  s(      88!s1vv888888r7   Nc              3  4   K   | ]}t          |          V  d S r   r  )r   is     r5   rJ  zCDiscordAdapter._canonicalize_app_command_payload.<locals>.<genexpr>x  s(      99!s1vv999999r7   c                d    g | ],}t          |t                                        |          -S r;   r   r    _canonicalize_app_command_optionr   itemrl   s     r5   r   zDDiscordAdapter._canonicalize_app_command_payload.<locals>.<listcomp>z  G       dD))55d;;  r7   options)	rm  r>   r  r  r  r  r  r  r  )r   r   r(   _normalize_permissionsr8   sorted)rl   r  r  r  s   `   r5   !_canonicalize_app_command_payloadz0DiscordAdapter._canonicalize_app_command_payloadi  sj   ;;z**#KK(;<<FA..3!44FB//5266w{{="==CDD*.*E*E899+ + "'++ot"D"DEEVU3344<DN88x888888$=NX99'8999999TX   #KK	266<"  
 
 	
r7   valuer	   Optional[str]c                (    | dS t          |           S )zDiscord emits default_member_permissions as str server-side but discord.py
        sets it as int locally. Normalize to str-or-None so the comparison is stable.Nr(   )r  s    r5   r  z%DiscordAdapter._normalize_permissions  s     =45zzr7   commandc                ,   t          |                                          }t          |dd          }|t          |          |d<   t          |dd          }|t          |           |d<   t          |dd          }|t          |d|          |d<   |S )u  Build a canonical-ready dict from an AppCommand.

        discord.py's AppCommand.to_dict() does NOT include nsfw,
        dm_permission, or default_member_permissions (they live only on the
        attributes). Pull them from the attributes so the canonicalizer sees
        the real server-side values instead of defaults — otherwise any
        command using non-default permissions would diff on every startup.
        r  N
guild_onlyr  r  r  )r   to_dictr:  r8   )rl   r  r  r  r  default_permissionss         r5   _existing_command_to_payloadz+DiscordAdapter._existing_command_to_payload  s     w(())w--"4jjGFOWlD99
!+/
+;+;';GO$%g/KTRR*4;#W.A5 5G01 r7   c                    t          |                    dd          pd          t          |                    dd          pd          t          |                    dd          pd          t          |                    dd                    t          |                    dd                    d	 |                    d
g           pg D             t	          |                    dg           pg           |                    d          |                    d          |                    d          |                    d           fd|                    dg           pg D             dS )Nrm  r   r>   rA   r  requiredFautocompletec                    g | ]R}t          |t                    t          |                    d d          pd          |                    d          dSS )r>   rA   r  r>   r  )r   r   r(   r   )r   choices     r5   r   zCDiscordAdapter._canonicalize_app_command_option.<locals>.<listcomp>  sn       
 fd++

62 6 6 <"==#ZZ00   r7   choiceschannel_types	min_value	max_value
min_length
max_lengthc                d    g | ],}t          |t                                        |          -S r;   r  r  s     r5   r   zCDiscordAdapter._canonicalize_app_command_option.<locals>.<listcomp>  r  r7   r  )rm  r>   r  r  r  r  r  r  r  r  r  r  )r   r   r(   r8   r   )rl   r  s   ` r5   r  z/DiscordAdapter._canonicalize_app_command_option  sd   FA..3!44FB//5266w{{="==CDDW[[U;;<< ^U!C!CDD 
 &kk)R88>B   "'++or"B"B"HbII [11 [11!++l33!++l33   #KK	266<"  '
 
 	
r7   c                \    |                      |          }|d         |d         |d         dS )z;Fields supported by discord.py's edit_global_command route.r>   r  r  )r>   r  r  )r  )rl   r  	canonicals      r5   _patchable_app_command_payloadz-DiscordAdapter._patchable_app_command_payload  s<    ::7CC	f%$]3 +
 
 	
r7   Dict[str, int]c                  K   | j         s	dddddddS | j         j        t          | j         dd          p$t          t          | j         dd          dd          }|st          d          fd                                D             }d	 |D             }                                 d{V }d
 |D             }d}d}d}d}	d}
| j         j        }|                                D ]!\  }}|                    |d          }|"|	                    ||           d{V  |	dz  }	@| 
                    |          }|                     |          }|                     |          }||k    r|dz  }|                     |          |                     |          k    rC|                    ||j                   d{V  |	                    ||           d{V  |dz  }|                    ||j        |           d{V  |dz  }#|                                D ](}|                    ||j                   d{V  |
dz  }
)t#          |          ||||	|
dS )zHDiff existing global commands and only mutate the commands that changed.r   )r  r  r  r  r  r  application_idNr   r   z<Discord application ID is unavailable for slash command syncc                :    g | ]}|                               S r;   )r  )r   r  r  s     r5   r   z<DiscordAdapter._safe_sync_slash_commands.<locals>.<listcomp>  s%    UUUgGOOD11UUUr7   c           	         i | ]_}t          |                    d d          pd          t          |                    dd          pd                                          f|`S )rm  r   r>   rA   )r   r   r(   r4   )r   r  s     r5   
<dictcomp>z<DiscordAdapter._safe_sync_slash_commands.<locals>.<dictcomp>  sr     
 
 
 VQ'',1--s7;;vr3J3J3Pb/Q/Q/W/W/Y/YZ\c
 
 
r7   c                    i | ]i}t          t          t          |d d          dt          |d d                    pd          t          |j        pd                                          f|jS )rm  Nr  r   rA   )r   r:  r(   r>   r4   )r   r  s     r5   r  z<DiscordAdapter._safe_sync_slash_commands.<locals>.<dictcomp>  s     
 
 

  GGGVT::GWWV\^_E`E`aafefggGL&B''--// 	
 
 
r7   r   )r!  r  r:  RuntimeErrorget_commandsfetch_commandshttpitemsr   upsert_global_commandr  r  r  delete_global_commandr   edit_global_commandvaluesr   )rl   app_iddesired_payloadsdesired_by_keyexisting_commandsexisting_by_keyr  r  r  r  r  r  keydesiredcurrentcurrent_existing_payloadcurrent_payloaddesired_payloadr  s                     @r5   r  z(DiscordAdapter._safe_sync_slash_commands  s8     | 	   | '7>>z''RVR^`fhlJmJmosuyBzBz 	_]^^^UUUUARARATATUUU
 
+
 
 
 #'"5"5"7"7777777
 

 -
 
 
 		| *0022 	 	LC%))#t44G00AAAAAAAAA1'+'H'H'Q'Q$"DDE]^^O"DDWMMO/11Q	223KLLPTPsPst{P|P|||00DDDDDDDDD00AAAAAAAAAQ	**67:wGGGGGGGGGqLGG&--// 	 	G,,VWZ@@@@@@@@@qLGG )**""
 
 	
r7   r\  emojic                   K   |rt          |d          sdS 	 |                    |           d{V  dS # t          $ r-}t                              d| j        ||           Y d}~dS d}~ww xY w)z+Add an emoji reaction to a Discord message.add_reactionFNTz![%s] add_reaction failed (%s): %s)r   r  r~   rx   r   r>   rl   r\  r
  r   s       r5   _add_reactionzDiscordAdapter._add_reaction  s       	gg~>> 	5	&&u---------4 	 	 	LL<diPQRRR55555	s   5 
A,"A''A,c                  K   |r#t          |d          r| j        r| j        j        sdS 	 |                    || j        j                   d{V  dS # t          $ r-}t
                              d| j        ||           Y d}~dS d}~ww xY w)z;Remove the bot's own emoji reaction from a Discord message.remove_reactionFNTz$[%s] remove_reaction failed (%s): %s)r   r!  r   r  r~   rx   r   r>   r  s       r5   _remove_reactionzDiscordAdapter._remove_reaction  s       	gg/@AA 	 	]a]i]n 	5	))%1BCCCCCCCCC4 	 	 	LL?ESTUUU55555	s   &A 
B
"BB
c                T    t          j        dd                                          dvS )z6Check if message reactions are enabled via config/env.DISCORD_REACTIONSrC   )false0no)rG   rH   r4   r   s    r5   _reactions_enabledz!DiscordAdapter._reactions_enabled$  s'    y,f55;;==EYYYr7   r  r   c                   K   |                                  sdS |j        }t          |d          r|                     |d           d{V  dS dS )z>Add an in-progress reaction for normal Discord message events.Nr     👀)r  raw_messager   r  )rl   r  r\  s      r5   on_processing_startz"DiscordAdapter.on_processing_start(  sq      &&(( 	F#7N++ 	6$$Wf55555555555	6 	6r7   outcomer   c                X  K   |                                  sdS |j        }t          |d          rx|                     |d           d{V  |t          j        k    r|                     |d           d{V  dS |t          j        k    r |                     |d           d{V  dS dS dS )zCSwap the in-progress reaction for a final success/failure reaction.Nr  r  u   ✅u   ❌)r  r  r   r  r   SUCCESSr  FAILURE)rl   r  r  r\  s       r5   on_processing_completez%DiscordAdapter.on_processing_complete0  s      &&(( 	F#7N++ 	9''888888888+333((%88888888888-555((%88888888888	9 	9 65r7   Nchat_idcontentreply_tometadataOptional[Dict[str, Any]]r   c                  K   | j         st          dd          S 	 d}|r|                    d          r|d         }|rn| j                             t	          |                    }|s-| j                             t	          |                     d{V }|st          dd| d          S nm| j                             t	          |                    }|s-| j                             t	          |                     d{V }|st          dd| d          S |                     |          r|                     ||           d{V S |                     |          }| 	                    || j
                  }g }	d}
|r| j        d	k    r	 |                    t	          |                     d{V }t          |d
          r|                    d          }
n|}
n2# t          $ r%}t                               d|           Y d}~nd}~ww xY wt%          |          D ]\  }}| j        dk    r|
}n
|dk    r|
nd}	 |                    ||           d{V }nv# t          $ ri}t)          |          }|Md|v rd|v sd|v rAt                               d| j        |           d}
|                    |d           d{V }n Y d}~nd}~ww xY w|	                    t)          |j                             t          d|	r|	d         ndd|	i          S # t          $ rK}t                               d| j        |d           t          dt)          |                    cY d}~S d}~ww xY w)u1  Send a message to a Discord channel or thread.

        When metadata contains a thread_id, the message is sent to that
        thread instead of the parent channel identified by chat_id.

        Forum channels (type 15) reject direct messages — a thread post is
        created automatically.
        FNot connectedsuccessr  N	thread_idzThread 
 not foundChannel r   to_reference)fail_if_not_existsz$Could not fetch reply-to message: %sallr   )r"  	referencezerror code: 50035z Cannot reply to a system messagezerror code: 10008zX[%s] Reply target %s rejected the reply reference; retrying send without reply referenceTmessage_idsr)  
message_idraw_responsez'[%s] Failed to send Discord message: %sr  )r!  r   r   get_channelr   fetch_channel_is_forum_parent_send_to_forumformat_messagetruncate_messageMAX_MESSAGE_LENGTHr;  fetch_messager   r-  r~   rx   r   	enumeratesendr(   r   r>   r   r   r  )rl   r!  r"  r#  r$  r*  r   	formattedchunksr1  r0  ref_msgr   r  chunkchunk_referencer   err_texts                     r5   r>  zDiscordAdapter.send<  s      | 	De?CCCCV	;I 2HLL55 2$[1	 [,223y>>BB O$(L$>$>s9~~$N$NNNNNNNG \%e;ZY;Z;Z;Z[[[[\ ,223w<<@@ M$(L$>$>s7||$L$LLLLLLLG [%e;Yg;Y;Y;YZZZZ $$W-- C!00'BBBBBBBBB ++G44I**9d6MNNFKI LD/588L$+$9$9#h--$H$HHHHHHHGw77 ,$+$8$8E$8$R$R		$+	  L L LLL!GKKKKKKKKL &f-- "0 "05&%//&/OO3466iitO ' %"1 !- ! !      CC !   "1vvH'3 !4x ? ?$F($R$R2h>> v I$  
 %)	$+LL$)&* %1 % %      
  %0 ""3sv;;////-8B;q>>d+[9     	; 	; 	;LLBDIq[_L```e3q66:::::::::	;s   BL ,A-L 0L AL AG L 
H)H	L 	H/L >IL 
K&AK
L 
KAL 
M-"A M("M-(M-forum_channelc                &  K   ddl m} |                     |          }|                     || j                  } ||          }|r|d         n|}	 |                    ||           d{V }nR# t          $ rE}	t                              d| j	        |j
        |	           t          dd|	           cY d}	~	S d}	~	ww xY wt          |d	          r|nt          |d
d          }
t          t          |
dt          |dd                              }t          |dd          }|rt          t          |d|                    n|}|g}g }|dd         D ]}	 |
                    |           d{V }|                    t          |j
                             G# t          $ rH}	d| d|	 }t                              d| j	        |           |                    |           Y d}	~	d}	~	ww xY w||d}|r||d<   t          d|d         |          S )a  Create a thread post in a forum channel with the message as starter content.

        Forum channels (type 15) don't support direct messages.  Instead we
        POST to /channels/{forum_id}/threads with a thread name derived from
        the first line of the message.  Any follow-up chunk failures are
        reported in ``raw_response['warnings']`` so the caller can surface
        partial-send issues.
        r   _derive_forum_thread_name)r>   r"  Nz,[%s] Failed to create forum thread in %s: %sFForum thread creation failed: r(  r>  threadr   rA   r\  r   r"  z/Failed to send follow-up chunk to forum thread z: z[%s] %s)r1  r*  warningsTr2  )tools.send_message_toolrH  r9  r:  r;  create_threadr~   rx   r  r>   r   r   r   r:  r(   r>  r   r   )rl   rE  r"  rH  r?  r@  thread_namestarter_contentrJ  r   thread_channelr*  starter_msgr3  r1  rL  rB  r   r   r4  s                       r5   r8  zDiscordAdapter._send_to_forum  s      	FEEEEE''00	&&y$2IJJ//88'->&));	Y(66 ' 7        FF  	Y 	Y 	YLLGTaTdfghhhe3WTU3W3WXXXXXXXXX	Y $+66#:#:_PXZ^@_@_gfdB6O6OPPQQ	fi66CN]SdI>>???T]
 "l ABBZ 	) 	)E)*///>>>>>>>>""3sv;;//// ) ) )\I\\YZ\\y$)W===(((((((()
 8CQZ'['[ 	0'/L$"1~%
 
 
 	
s7   A/ /
B>9:B93B>9B>AF
G,$>G''G,rA   )rO  r"  filefilesrO  rS  rT  Optional[list]c               ,  K   ddl m} |sj|pd}|                                s1|t          |dd          pd}n|rt          |d         dd          pd}|                                r ||          nd}d|i}|r||d<   |||d	<   |r||d
<   	  |j        di | d{V }	n\# t
          $ rO}
t                              d| j        t          |dd          |
           t          dd|
           cY d}
~
S d}
~
ww xY wt          |	d          r|	nt          |	dd          }t          t          |dt          |	dd                              }t          |	dd          }|rt          t          |d|                    n|}t          d|d|i          S )au  Create a forum thread whose starter message carries file attachments.

        Used by the send_voice / send_image_file / send_document paths when
        the target channel is a forum (type 15).  ``create_thread`` on a
        ForumChannel accepts the same file/files/content kwargs as
        ``channel.send``, creating the thread and starter message atomically.
        r   rG  rA   NfilenamezNew Postr>   r"  rS  rT  z6[%s] Failed to create forum thread with file in %s: %sr   ?FrI  r(  r>  rJ  r\  Tr*  r2  r;   )rM  rH  r/   r:  rN  r~   rx   r  r>   r   r   r(   )rl   rE  rO  r"  rS  rT  rH  hintkwargsrJ  r   rQ  r*  rR  r3  s                  r5   _forum_post_filezDiscordAdapter._forum_post_file  s:       	FEEEEE 		Z =bD::<< C#"4R88>BDD C"58Z<<BD=AZZ\\Y33D999zK"(+!6 	( 'F9!F6N 	$#F7O		Y6=6@@@@@@@@@@FF 	Y 	Y 	YLLH	tS11	   e3WTU3W3WXXXXXXXXX	Y $+66#:#:_PXZ^@_@_gfdB6O6OPPQQ	fi66CN]SdI>>???T]
!%y1
 
 
 	
s   B# #
C<-AC71C<7C<F)finalizer3  r\  c                 K   | j         st          dd          S 	 | j                             t          |                    }|s-| j                             t          |                     d{V }|                    t          |                     d{V }|                     |          }t          |          | j        k    r|d| j        dz
           dz   }|	                    |           d{V  t          d|	          S # t          $ rL}t                              d
| j        ||d           t          dt          |                    cY d}~S d}~ww xY w)z'Edit a previously sent Discord message.Fr'  r(  N   ...rK  Tr)  r3  z*[%s] Failed to edit Discord message %s: %sr  )r!  r   r5  r   r6  r<  r9  r   r;  editr~   rx   r  r>   r(   )	rl   r!  r3  r"  r\  r   r   r?  r   s	            r5   edit_messagezDiscordAdapter.edit_message  s      | 	De?CCCC	;l..s7||<<G I $ : :3w<< H HHHHHHH--c*oo>>>>>>>>C++G44I9~~ 777%&Bt'>'B&BCeK	((9(---------dzBBBB 	; 	; 	;LLEtyR\^_jnLoooe3q66:::::::::	;s   C,D	 	
EAEEE	file_pathcaption	file_namec                  K   | j         st          dd          S | j                             t          |                    }|s-| j                             t          |                     d{V }|st          dd| d          S |pt
          j                            |          }t          |d          5 }t          j
        ||          }|                     |          r>|                     ||pd	                                |
           d{V cddd           S |                    |r|nd|
           d{V }	ddd           n# 1 swxY w Y   t          dt          |	j                            S )u   Send a local file as a Discord attachment.

        Forum channels (type 15) get a new thread whose starter message
        carries the file — they reject direct POST /messages.
        Fr'  r(  Nr,  r+  rbrW  rA   r"  rS  Tr`  )r!  r   r5  r   r6  rG   r  basenameopenrS   Filer7  r[  r/   r>  r(   r   )
rl   r!  rc  rd  re  r   rW  fhrS  r   s
             r5   _send_file_attachmentz$DiscordAdapter._send_file_attachment1  s$      | 	De?CCCC,**3w<<88 	E L66s7||DDDDDDDDG 	Se3Qg3Q3Q3QRRRR; 0 0 ; ;)T"" 	Vb<X666D$$W-- !22$]1133 3        	V 	V 	V 	V 	V 	V 	V 	V  -IWWTPTUUUUUUUUC	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V 	V $3sv;;????s   9AE#!EEE        imagesList[Tuple[str, str]]human_delayr)  c                P  K   | j         sdS sdS 	 ddl}ddl}ddlm} n;# t
          $ r. t                                          |||           d{V  Y dS w xY w	 | j                             t          |                    }|s-| j         
                    t          |                     d{V }|s#t                              d| j        |           dS nc# t
          $ rV}	t                              d| j        |	           t                                          |||           d{V  Y d}	~	dS d}	~	ww xY wdfdt          dt                              D             }
t!          |
          D ]\  }}|dk    r |dk    rt#          j        |           d{V  g }g }d}	 |D ]\  }}|r|                    |           |                    d          r ||d	d                   }t*          j                            |          s"t                              d
| j        |           |                    |                    |t*          j                            |                               t5          |          s!t                              d| j                   	 ddl}ddlm}m}  |d          } ||          \  }}| |j        d(i |} |j         |fd|!                    d          i|4 d{V 	 }|j"        dk    rCt                              d| j        |j"        |dd                    	 ddd          d{V  |#                                 d{V }|j$                             dd          }d}d|v sd|v rd}nd|v rd}nd|v rd}|                    |                    |%                    |          dt          |           d|                      ddd          d{V  n# 1 d{V swxY w Y   r# t
          $ r5}t                              d| j        |dd         |           Y d}~d}~ww xY w|s3	 |.	 |&                                 d{V  # t
          $ r Y w xY w|r|d         nd}t          '                    d | j        t          |          |d!z   t          |
                     | (                    |          r3| )                    ||pd"*                                |#           d{V  n|+                    ||#           d{V  nw# t
          $ rj}	t                              d$| j        |d!z   t          |
          |	d%&           t                                          ||||'           d{V  Y d}	~	nd}	~	ww xY w|.	 |&                                 d{V  f# t
          $ r Y sw xY wy# |,	 |&                                 d{V  w # t
          $ r Y w w xY ww xY wdS ))a  Send a batch of images as a single Discord message with multiple attachments.

        Discord permits up to 10 file attachments per message. Batches are
        chunked accordingly. URL images are downloaded into memory and
        uploaded as inline attachments (same pattern as ``send_image`` so
        they render inline, not as bare links). Local files are opened
        directly. On per-chunk failure the remaining images in that chunk
        fall back to the base per-image loop.
        Nr   )unquotez.[%s] Channel %s not found for multi-image sendz7[%s] Failed to resolve channel for multi-image send: %sr   c                *    g | ]}||z            S r;   r;   )r   r  CHUNKrp  s     r5   r   z7DiscordAdapter.send_multiple_images.<locals>.<listcomp>|  s&    LLL!&1u9%LLLr7   zfile://   z[%s] Skipping missing image: %srh  z&[%s] Blocked unsafe image URL in batchrK  proxy_kwargs_for_aiohttprM  rN  r  r  r     z4[%s] Failed to download image (HTTP %d) in batch: %sP   content-type	image/pngpngjpegjpggifwebpimage_.z[%s] Download failed for %s: %sz@[%s] Sending %d image(s) as single Discord message (chunk %d/%d)r   rA   )r"  rT  zQ[%s] Multi-image Discord send failed (chunk %d/%d), falling back to per-image: %sTr  )rr  r;   ),r!  rS   iourllib.parsert  r~   r  send_multiple_imagesr5  r   r6  rx   r   r>   ranger   r=  r"  sleepr   r0   rG   r  existsrl  rj  r&   aiohttpr  rK  ry  ClientSessionr   ClientTimeoutstatusreadheadersBytesIOr  ry   r7  r[  r/   r>  )!rl   r!  rp  r$  rr  _discord_mod_io_unquoter   r   r@  	chunk_idxrB  rT  captionsaiohttp_session	image_urlalt_text
local_path_aiohttprK  ry  _proxy_sess_kw_req_kwrespr   ctextdl_errr"  rv  r>  s!     `                            @r5   r  z#DiscordAdapter.send_multiple_imagesR  s	       | 	F 	F	****8888888 	 	 	''..w+VVVVVVVVVFF	
	l..s7||<<G I $ : :3w<< H HHHHHHH OQUQZ\cddd  	 	 	NNTVZV_abccc''..w+VVVVVVVVVFFFFF	
 LLLLLuQFU/K/KLLL )& 1 1 R	 R	IuQ9q==mK000000000!E"$H"OK+0 *% *%'Ix 2 111 ++I66 '%%-Xim%<%<
!w~~j99 %"NN+LdiYcddd$\%6%6zBGL\L\]gLhLh%6%i%ijjjj*955 %"NN+SUYU^___$%6666jjjjjjjj%6%6%X%X%XF0H0H0P0P-Hg.62H(2H2T2T82T2T':': )( (3;3I3IPR3I3S3S(W^( ( y y y y y y y y!%#';##5#5$*NN(^(,	4;	#2#%& %& %& %-y y y y y y y y y y y y y .2YY[['8'8'8'8'8'8%)\%5%5nk%R%R&+#)R<<5B;;*/CC%*b[[*/CC%+r\\*0C %\->->s{{4?P?P[vdghmdndn[v[vqt[v[v->-w-w x x x%y y y y y y y y y y y y y y y y y y y y y y y y y y y&  ) % % %"NN+LdiYbcfdfcfYgioppp$HHHH%  2 #.-335555555555$    /- *2;(1++tVIs5zz9q=#f++  
 ((11 E//!(B 5 5 7 7# 0           ",,we,DDDDDDDDD f f fgIy1}c&kk1!    
 gg227E8Yd2eeeeeeeeeeeeeef #.-335555555555$    /?.-335555555555$    /]R	 R	s  # 4AAA9C 
D;%AD66D;-D U
.AP!<P
P!U
BP<P!
PP!PP!U
!
Q +*QU
Q  U
)R
RRB1U
	W3

V>A V94W39V>>W3W  
W.-W.3X#7XX#
X	X#X	X#
audio_pathc                x  K   | j                                         D ]\  }}t          |          t          |          k    rd|                     |          rOt                              d| j        |           |                     ||           d{V }t          |          c S  | j	        d||d| d{V S )zPlay auto-TTS audio.

        When the bot is in a voice channel for this chat's guild, play
        directly in the VC instead of sending as a file attachment.
        z,[%s] Playing TTS in voice channel (guild=%d)N)r)  )r!  r  r;   )
r.  r  r(   is_in_voice_channelrx   ry   r>   play_in_voice_channelr   
send_voice)rl   r!  r  rZ  gid
text_ch_idr)  s          r5   play_ttszDiscordAdapter.play_tts  s        $8>>@@ 	3 	3OC:#g,,..43K3KC3P3P.JDIWZ[[[ $ : :3
 K KKKKKKK!'222222$T_VWVVvVVVVVVVVVr7   c           	     `  K   	 ddl }| j                            t          |                    }|s-| j                            t          |                     d{V }|st          dd| d          S t          j                            |          st          dd|           S t          j        	                    |          }	t          |d          5 }
|
                                }ddd           n# 1 swxY w Y   |                     |          r[t          j        |                    |          |		          }|                     ||pd
                                |           d{V S 	 ddl}d}	 ddlm}  ||          }|j        j        }n0# t.          $ r# t1          dt3          |          dz            }Y nw xY wt5          dgdz            }|                    |                                          }ddl}|                    dddt?          |d          |dgd          }d|dd|dddg}| j        j         !                    t          j         "                    dd|j#                  |            d{V }t          d!tI          |d"                   #          S # t.          $ r}tJ          &                    d$|           t          j        |                    |          |		          }|'                    |%           d{V }t          d!tI          |j#                  #          cY d}~S d}~ww xY w# t.          $ rY}tJ          (                    d&| j)        |d!'           tU                      +                    |||||(           d{V cY d}~S d}~ww xY w))z(Send audio as a Discord file attachment.r   NFr,  r+  r(  zAudio file not found: rg  rh  rA   ri  g      @)OggOpus      ?g     @@      i    r  zvoice-message.oggr   )r   rW  duration_secswaveform)flagsattachmentspayload_jsonr  zfiles[0]z	audio/ogg)r>   r  rW  content_typePOSTz/channels/{channel_id}/messages
channel_id)formTr   r`  z3Voice message flag failed, falling back to file: %s)rS  z;[%s] Failed to send audio, falling back to base adapter: %sr  r$  ),r  r!  r5  r   r6  r   rG   r  r  rj  rk  r  r7  rS   rl  r  r[  r/   base64mutagen.oggopusr  ry   lengthr~   maxr   rq   	b64encoder   jsondumpsroundr  requestRouter   r(   rx   r   r>  r  r>   r  r  )rl   r!  r  rd  r#  r$  rZ  r  r   rW  r  	file_data
forum_filer  r  r  ry   waveform_byteswaveform_b64_jsonr  r  msg_data	voice_errrS  r   r   r>  s                              r5   r  zDiscordAdapter.send_voice  s     K	gIIIl..s7||<<G I $ : :3w<< H HHHHHHH W!%7U'7U7U7UVVVV7>>*-- ^!%7\PZ7\7\]]]]w''
33Hj$'' %1FFHH	% % % % % % % % % % % % % % % $$W-- $\"**Y*?*?(SSS
!22$]1133# 3         *H #F777777"7:..D$(I$4MM  F F F$'S^^f-D$E$EMMMF "'us{!3!3%//??FFHH$$$$++!!$7).}a)@)@$0	% % $' '   ,g>> *!*$7(3	  "&!2!:!:L&&v/P]d]g&hh "; " "       "$3x~;N;NOOOO H H HRT]^^^|BJJy$9$9HMMM#LLdL33333333!$3sv;;GGGGGGGGG	H
  	g 	g 	gLLVX\XacdosLttt++GZ(]e+ffffffffffffff	gs   A0M
 62M
 )/M
 C9-M
 9C==M
  C=A2M
 5J0 <F J0 *GJ0 GC(J0 0
M:BM<M=M
 MM
 

N-AN("N-(N-c                  K   | j         rt          sdS |j        j        }| j                            |t          j                              4 d{V  | j        	                    |          }|r|
                                r|j        j        |j        k    r)|                     |           	 ddd          d{V  dS |                    |           d{V  |                     |           	 ddd          d{V  dS |                                 d{V }|| j        |<   |                     |           	 t          || j                  }|                                 || j        |<   t          j        |                     |                    | j        |<   n2# t,          $ r%}t.                              d|           Y d}~nd}~ww xY w	 ddd          d{V  dS # 1 d{V swxY w Y   dS )z6Join a Discord voice channel. Returns True on success.FNT)rY   z"Voice receiver failed to start: %s)r!  r:   ry  r   r(  
setdefaultr"  rd   r'  r   is_connectedr   _reset_voice_timeoutmove_tor  rW   r]   r{   r1  ensure_future_voice_listen_loopr2  r~   rx   r   )rl   r   r  existingvcreceiverr   s          r5   join_voice_channelz!DiscordAdapter.join_voice_channel?  s"     | 	#4 	5=#$//',..II 	 	 	 	 	 	 	 	*..x88H H1133 #&'*44--h777	 	 	 	 	 	 	 	 	 	 	 	 	 	 &&w/////////))(333	 	 	 	 	 	 	 	 	 	 	 	 	 	 ((((((((B,.D)%%h///H(d>TUUU   2:%h/5<5J++H556 6(22  H H HCQGGGGGGGGH 5	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	sJ   AG( 1G(9G(>A#F"!G("
G,GG(GG((
G25G2r  r   c                  K   | j                             |t          j                              4 d{V  | j                            |d          }|r|                                 | j                            |d          }|r|                                 | j	                            |d          }|r.|
                                r|                                 d{V  | j                            |d          }|r|                                 | j                            |d           | j                            |d           ddd          d{V  dS # 1 d{V swxY w Y   dS )z-Disconnect from the voice channel in a guild.N)r(  r  r"  rd   r1  r   r   r2  rW  r'  r  r  r0  r.  r/  )rl   r  r  listen_taskr  tasks         r5   r  z"DiscordAdapter.leave_voice_channela  s     $//',..II 	4 	4 	4 	4 	4 	4 	4 	4,004@@H  266xFFK %""$$$$((488B &boo'' &mmoo%%%%%%%,004@@D %))(D999##Hd333!	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4 	4s   DE
E'*E'r   c                  	K   | j                             |          }|r|                                sdS | j                            |          }|r|                                 	 t          j                    }|                                r|t          j                    |z
  | j        k    r/t          
                    d           |                                 n.t          j        d           d{V  |                                |t          j                    t          j                    		fd}t!          j        |          }t!          j        |d          }|                    ||           	 t          j                                        | j        	           d{V  nI# t          j        $ r7 t          
                    d
| j                   |                                 Y nw xY w|                     |           	 |r|                                 dS dS # |r|                                 w w xY w)z2Play an audio file in the connected voice channel.Fz1Timed out waiting for previous playback to finishg?Nc                v    | rt                               d|                                j                   d S )NzVoice playback error: %s)rx   r  call_soon_threadsaferZ   )r  rV  loops    r5   _afterz4DiscordAdapter.play_in_voice_channel.<locals>._after  s=     DLL!;UCCC))$(33333r7   r  )volume)r}  r_  z"Voice playback timed out after %dsT)r'  r   r  r1  r   r   r   
is_playingPLAYBACK_TIMEOUTrx   r   r   r"  r  r#  get_running_looprS   FFmpegPCMAudioPCMVolumeTransformerplayrh  ri  rj  r  r   )
rl   r  r  r  r  
wait_startr  sourcerV  r  s
           @@r5   r  z$DiscordAdapter.play_in_voice_channelx  s\      $$X.. 	** 	5 (,,X66 	NN	"))J--// )>##j043HHHNN#VWWWGGIIImC((((((((( --// ) =??D+--D4 4 4 4 4 4
 +J77F1&EEEFGGF&G)))&tyy{{D<QRRRRRRRRRRR'   CTEZ[[[					 %%h/// "!!!!!" "x "!!!!"s2   (DH& 93F- ,H& -AG30H& 2G33H& &H?r   c                   K   | j         sdS | j                             |          }|sdS |                    t          |                    }|r|j        sdS |j        j        S )z;Return the voice channel the user is currently in, or None.N)r!  	get_guild
get_memberr   voicer   )rl   r  r   ry  r{  s        r5   get_user_voice_channelz%DiscordAdapter.get_user_voice_channel  sq      | 	4&&x00 	4!!#g,,// 	V\ 	4|##r7   c                    | j                             |d          }|r|                                 t          j        |                     |                    | j         |<   dS )z+Reset the auto-disconnect inactivity timer.N)r0  r   rW  r"  r  _voice_timeout_handler)rl   r  r  s      r5   r  z#DiscordAdapter._reset_voice_timeout  s`    (,,Xt<< 	KKMMM.5.C''11/
 /
!(+++r7   c                  K   	 t          j        | j                   d{V  n# t           j        $ r Y dS w xY w| j                            |          }|                     |           d{V  | j        r6|r4	 |                     t          |                     n# t          $ r Y nw xY w|rR| j
        rM| j
                            |          }|r3	 |                    d           d{V  dS # t          $ r Y dS w xY wdS dS dS )z:Auto-disconnect after VOICE_TIMEOUT seconds of inactivity.Nz(Left voice channel (inactivity timeout).)r"  r  VOICE_TIMEOUTr  r.  r   r  r4  r(   r~   r!  r5  r>  )rl   r  r  chs       r5   r  z%DiscordAdapter._voice_timeout_handler  s     	- 23333333333% 	 	 	FF	.228<<
&&x000000000$ 	 	))#j//::::    	$, 	))*55B ''"LMMMMMMMMMMM    DD	 	 	 	 s0   $ 779"B 
B)(B)C/ /
C=<C=c                f    | j                             |          }|duo|                                S )z?Check if the bot is connected to a voice channel in this guild.N)r'  r   r  )rl   r  r  s      r5   r  z"DiscordAdapter.is_in_voice_channel  s1     $$X..~3"//"3"33r7   c                   | j                             |          }|r|                                sdS |j        }|sdS g }| j        r| j        j        nd}|j        D ]=}|r|j        |j        k    r|                    |j        |j	        |j
        d           >t                      }| j                            |          }|rt          j                    }	|j        5  |j                                        D ]?\  }
}|	|z
  dk     r1|j                            |
          }|r|                    |           @	 ddd           n# 1 swxY w Y   |D ]}|d         |v |d<   |j        t+          |          |t+          |          dS )a  Return voice channel awareness info for the given guild.

        Returns None if the bot is not in a voice channel.  Otherwise
        returns a dict with channel name, member list, count, and
        currently-speaking user IDs (from SSRC mapping).
        N)r   rz  is_botg       @r   is_speaking)channel_namemember_countr   speaking_count)r'  r   r  r   r!  r   r   r   r   rz  r`  rZ   r1  r   r   re   rh   r  rb   addr>   r   )rl   r  r  r   members_infobot_userr   speaking_user_idsr  r   rt   last_tr   ry   s                 r5   get_voice_channel_infoz%DiscordAdapter.get_voice_channel_info  s      $$X.. 	** 	4* 	4 (,>4<$$$ 	 	A ADHK//4 !%! !     "%(,,X66 	7.""C 7 7$,$>$D$D$F$F 7 7LD&V|c))&488>> 7-11#66677 7 7 7 7 7 7 7 7 7 7 7 7 7 7 ! 	G 	GD"&y/5F"FD $L--#!"344	
 
 	
s   AEE	E	c                    |                      |          }|sdS d|d          d|d          dg}|d         D ].}|d         rd	nd}|                    d
|d          |            /d                    |          S )zReturn a human-readable voice channel context string.

        Suitable for injection into the system/ephemeral prompt so the
        agent is always aware of voice channel state.
        rA   z[Voice channel: #r      — r  z participant(s)]r   r  z (speaking)z  - rz  
)r  r   join)rl   r  ry   partsr   r  s         r5   get_voice_channel_contextz(DiscordAdapter.get_voice_channel_context  s     **844 	2fT.%9ff^@Tfffgi 	= 	=A&'&6>]]BFLL;. 1;6;;<<<<yyr7   r   c                  K   | j                             |          }|sdS t          j                    }	 |j        rt          j        d           d{V  t          j                    }||z
  | j        k    r^|}	 | j                            |          }|r.|	                                r|j
                            d           n# t          $ r Y nw xY w|                                }|D ]E\  }}|                     t          |                    s(|                     |||           d{V  F|j        dS dS # t
          j        $ r Y dS t          $ r(}	t$                              d|	d           Y d}	~	dS d}	~	ww xY w)z=Periodically check for completed utterances and process them.Ng?s   zVoice listen loop error: %sTr  )r1  r   r   r   r^   r"  r  _KEEPALIVE_INTERVALr'  r  rp   send_packetr~   r   ro  r(   _process_voice_inputr  rx   r  )
rl   r  r  last_keepaliver   r  r   r   r   r   s
             r5   r  z!DiscordAdapter._voice_listen_loop  s     (,,X66 	F))	J# QmC((((((((( n&&'4+CCC%(N!044X>> H"//"3"3 HN66GGG$    %2244	)2 Q Q%GX00W>> ! 33HgxPPPPPPPPPP' # Q Q Q Q Q( % 	 	 	DD 	J 	J 	JLL6DLIIIIIIIII	JsD   AD< :A
C D< 
CD< CA&D< <E?	E?E::E?r   rq   c                  K   ddl m} t          j        ddd          }|j        }|                                 	 t          j        t          j	        ||           d{V  ddl
m} t          j        ||           d{V }|                    d	          s)	 	 t          j        |           dS # t          $ r Y dS w xY w|                    d
d                                          }	|	r ||	          r)	 	 t          j        |           dS # t          $ r Y dS w xY wt"                              d||	dd                    | j        r|                     |||	           d{V  n4# t(          $ r'}
t"                              d|
d           Y d}
~
nd}
~
ww xY w	 t          j        |           dS # t          $ r Y dS w xY w# 	 t          j        |           w # t          $ r Y w w xY wxY w)z&Convert PCM -> WAV -> STT -> callback.r   )is_whisper_hallucination.wav
vc_listen_F)r   prefixr   N)transcribe_audior)  
transcriptrA   zVoice input from user %d: %sd   )r  r   r  z!Voice input processing failed: %sTr  )tools.voice_moder  r  r  r>   r  r"  	to_threadrW   r  tools.transcription_toolsr  r   rG   r  r  r/   rx   ry   r3  r~   r   )rl   r  r   r   r  tmp_fwav_pathr  resultr  r   s              r5   r	  z#DiscordAdapter._process_voice_input?  s     ======+6,W\]]]:	#M$<hQQQQQQQQQBBBBBB",-=xHHHHHHHHF::i((  	(#####   #  L"55;;==J !9!9*!E!E 	(#####    KK6DSDAQRRR) 00%#) 1         
  	R 	R 	RNN>DNQQQQQQQQ	R	(#####   	(####   s   AE) B0 0
B>=B>5E) 8D 
DDA	E) (G )
F3FG FG F4 4
GGG,GG,
G)&G,(G))G,c                V   t          | dt                                }t          | dt                                t          |          }t                    }|s|sdS |r||v rdS |r|t          |dd          nd}|rt          fd|D                       rdS | j        	 t          |          }n# t          t          f$ r d}Y nw xY w|X| j        j        D ]K}|	                    |          }	|	t          |	dd          pg }
t          fd|
D                       r dS LdS )	a  Check if user is allowed via DISCORD_ALLOWED_USERS or DISCORD_ALLOWED_ROLES.

        Uses OR semantics: if the user matches EITHER allowlist, they're allowed.
        If both allowlists are empty, everyone is allowed (backwards compatible).
        When author is a Member, checks .roles directly; otherwise falls back
        to scanning the bot's mutual guilds for a Member record.
        r]   r%  TNrP   c              3  >   K   | ]}t          |d d          v V  dS r   Nr:  r   rallowed_roless     r5   rJ  z2DiscordAdapter._is_allowed_user.<locals>.<genexpr>{  s4      UU1wq$-->UUUUUUr7   c              3  >   K   | ]}t          |d d          v V  dS r  r  r  s     r5   rJ  z2DiscordAdapter._is_allowed_user.<locals>.<genexpr>  s4      XX1wq$55FXXXXXXr7   F)
r:  rZ   r8   rq  r!  r   	TypeError
ValueErrorguildsr  )rl   r   rl  allowed_users	has_users	has_rolesdirect_rolesuid_intry  r   m_rolesr  s              @r5   ro  zDiscordAdapter._is_allowed_userb  s     &9355AA&9355AA''	''	 	 	4 	M114 	(=C=O767D999UYL  UUUUUUUUU  4|'#!'llGG!:. # # #"GGG#&!%!4 ( (!,,W559$")!Wd";";"ArXXXXPWXXXXX (#'44(us   &B6 6CCinteraction'discord.Interaction'Tuple[bool, Optional[str]]c                &   t          |dd          }|t          |t          j                  nd}|s/t          |dd          pt          |dd          }t	                      }|u|                    t          |                     t          |t          j                  r9|                     |          }|r"|                    t          |                     t          j
        dd          }|r.d |                    d	          D             }d
|vr|sdS ||z  sdS t          j
        dd          }	|	r,|r*d |	                    d	          D             }
d
|
v s||
z  rdS t          |dd          }t          | dt	                                pt	                      }t          | dt	                                pt	                      }|t          |dd          |s|rdS dS t          |j                  }|                     ||          sdS dS )af  Evaluate slash authorization without producing any response.

        Returns ``(allowed, reason)``. ``reason`` is populated only when
        ``allowed`` is False. This is the shared core used by both the
        responding wrapper (``_check_slash_authorization``) and side-effect-
        free callers like the ``/skill`` autocomplete callback, which must
        return an empty list for unauthorized users instead of leaking an
        ephemeral rejection per-keystroke.

        Fail-closed semantics for malformed payloads: when an allowlist is
        configured but the interaction is missing the data needed to
        evaluate it (no channel id with channel policy active, no user
        with user/role policy active), the gate REJECTS rather than
        falling through. Without these guards a guild interaction that
        happens to deserialize without a channel id would silently bypass
        ``DISCORD_ALLOWED_CHANNELS`` and a payload missing ``user`` would
        raise ``AttributeError`` in the user check below, surfacing as
        an opaque interaction failure rather than a clean rejection.
        r   NFr  r   DISCORD_ALLOWED_CHANNELSrA   c                ^    h | ]*}|                                 |                                 +S r;   r/   r  s     r5   rC  z?DiscordAdapter._evaluate_slash_authorization.<locals>.<setcomp>  -    RRR		R17799RRRr7   rD  *)Fz;channel id missing with DISCORD_ALLOWED_CHANNELS configured)Fz'channel not in DISCORD_ALLOWED_CHANNELSDISCORD_IGNORED_CHANNELSc                ^    h | ]*}|                                 |                                 +S r;   r0  r  s     r5   rC  z?DiscordAdapter._evaluate_slash_authorization.<locals>.<setcomp>  r1  r7   )Fz#channel in DISCORD_IGNORED_CHANNELSr   r]   r%  )Fz2missing interaction.user with allowlist configured)TN)rl  )Fz9user not in DISCORD_ALLOWED_USERS / DISCORD_ALLOWED_ROLES)r:  r   rS   rp  rZ   r  r(   Thread_get_parent_channel_idrG   rH   r  r   ro  )rl   r*  chan_objin_dmchan_id_rawchannel_ids	parent_idallowed_rawr   ignored_rawignoredr   r$  r  r   s                  r5   _evaluate_slash_authorizationz,DiscordAdapter._evaluate_slash_authorization  sg   , ;	488;C;O
8W%6777UZ
  #	J!+|TBB g$G GK  #uuK&K 0 0111 h77 8 $ ; ;H E EI  8#I777)$>CCK RRRk.?.?.D.DRRRg%%&     ('1 RQQ
 )$>CCK J{ JRRk.?.?.D.DRRR'>>kG&;>II {FD11&9355AAJSUU&9355AAJSUU<74t44<
  U UTT<dg,,$$WT$:: 	 
 |r7   command_textc                ~   K   |                      |          \  }}|rdS |                     |||pd           d{V S )uO  Mirror on_message's user/role/channel gates onto a slash invocation.

        Returns True to proceed. Returns False *after* sending an ephemeral
        rejection, logging a warning, and scheduling a cross-platform admin
        alert — the caller must stop on False (the interaction has already
        been responded to).
        Tunauthorized)reasonN)r?  _reject_slash)rl   r*  r@  r   rC  s        r5   _check_slash_authorizationz)DiscordAdapter._check_slash_authorization  ss       <<[II 	4''f.F ( 
 
 
 
 
 
 
 
 	
r7   rC  c          
       K   t          |dd          }|0t          t          |dd                    }t          |dd          }nd}d}t          |dd          pt          t          |dd          dd          }t          |dd          }t                              d	||||||           	 |j                            d
d           d{V  n2# t          $ r%}	t                              d|	           Y d}	~	nd}	~	ww xY w	 t          j	        | 
                    ||||||                     n2# t          $ r%}	t                              d|	           Y d}	~	nd}	~	ww xY wdS )a  Send ephemeral reject + log warning + schedule admin alert. Returns False.

        Tolerates a missing ``interaction.user`` -- the fail-closed branch
        in ``_evaluate_slash_authorization`` deliberately routes here for
        malformed payloads (no user) when an allowlist is configured, and
        ``str(interaction.user.id)`` would raise AttributeError before the
        ephemeral rejection could be sent.
        r   Nr   rX  r>   r  r   r  zX[Discord] Unauthorized slash attempt: user=%s id=%s channel=%s guild=%s cmd=%r reason=%rz*You're not authorized to use this command.T	ephemeralz3[Discord] Could not send unauthorized ephemeral: %sz2[Discord] Could not schedule admin notify task: %sF)r:  r(   rx   r   responsesend_messager~   r   r"  rX  _notify_unauthorized_slash)
rl   r*  r@  rC  r   r   	user_namechan_idr  r   s
             r5   rD  zDiscordAdapter._reject_slash  s      {FD11'$c2233Gfc22IIGI+|T:: 
gKD114?
 ?
 ;
D99(w<	
 	
 	
	S&33< 4            	S 	S 	S LLNPQRRRRRRRR	S	R ? ?7GX|V! !      	R 	R 	RLLMqQQQQQQQQ	R us0   -"C 
C?C::C?,D0 0
E:EErL  c                p  K   t          | dd          }|sdS t          j        t          j        fD ]}	 |j                            |          }	|	s!|j                            |          }
|
rt          |
dd          sOd| d| d| d| d| d	| }|	                    t          |
j
                  |           d{V }t          |d
d          du r,t                              d|t          |dd                      dS # t          $ r&}t                              d||           Y d}~d}~ww xY wdS )a  Best-effort cross-platform alert to the gateway operator.

        Tries TELEGRAM first (most operators set TELEGRAM_HOME_CHANNEL),
        then SLACK. Silently no-ops if no other platform is configured
        with a home channel.

        A soft send failure -- adapter.send() returning a result with
        ``success=False`` rather than raising -- continues the fallback
        chain. Treating a SendResult(success=False) as delivered would
        mean a Telegram outage that the adapter politely surfaces (e.g.
        rate-limit, auth failure) silently swallows the alert without
        attempting Slack. Hard exceptions still take the same path via
        the except branch below.
        r&  Nr!  u0   ⚠️ Unauthorized Discord slash attempt
User:  (z)
Channel: z (guild z)
Command: z	
Reason: r)  FzP[Discord] Admin notify via %s returned success=False (error=%r); falling throughr  z([Discord] Admin notify via %s failed: %s)r:  r   TELEGRAMSLACKadaptersr   r  get_home_channelr>  r(   r!  rx   r   r~   )rl   rL  r   rM  r  r@  rC  runnertargetadapterhomer   r  r   s                 r5   rK  z)DiscordAdapter._notify_unauthorized_slash6  s     $ /66 	F((.9 	T 	TFT /--f55 }55f== 74D#A#A (&( (*1( ( '( (19( ( !-( (  &	( (   '||C,=,=sCCCCCCCC 69d33u<<LL7 > >  
  T T TGQRSSSSSSSST5	T 	Ts$   D-D?B D
D3D..D3
image_pathc                N  K   	 |                      |||           d{V S # t          $ r t          dd|           cY S t          $ rY}t                              d| j        |d           t                                          |||||           d{V cY d}~S d}~ww xY w)	z>Send a local image file natively as a Discord file attachment.NFzImage file not found: r(  zA[%s] Failed to send local image, falling back to base adapter: %sTr  r  )	rn  FileNotFoundErrorr   r~   rx   r  r>   r  send_image_file)rl   r!  rX  rd  r#  r$  r   r>  s          r5   r[  zDiscordAdapter.send_image_fileh  s      	l33GZQQQQQQQQQ  	Z 	Z 	Ze3XJ3X3XYYYYYY 	l 	l 	lLL\^b^gijuyLzzz00*gxbj0kkkkkkkkkkkkkk	l"   " B$	B$ABB$B$r  c                b  K   | j         st          dd          S t          |          sLt                              d| j                   t                                          |||||           d{V S 	 ddl}| j         	                    t          |                    }|s-| j                             t          |                     d{V }|st          dd| d	          S dd
lm}m}	  |d          }
 |	|
          \  }} |j        d"i |4 d{V } |j        |fd|                    d          i|4 d{V }|j        dk    rt'          d|j                   |                                 d{V }|j                            dd          }d}d|v sd|v rd}nd|v rd}nd|v rd}ddl}t/          j        |                    |          d|           }|                     |          rV|                     ||pd                                |           d{V cddd          d{V  cddd          d{V  S |                    |r|nd|           d{V }t          dt=          |j                            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@          $ rO t                              d| j        d            t                                          ||||           d{V cY S t&          $ rW}t          !                    d!| j        |d            t                                          ||||           d{V cY d}~S d}~ww xY w)#z4Send an image natively as a Discord file attachment.Fr'  r(  z7[%s] Blocked unsafe image URL during Discord send_imager  Nr   r,  r+  rx  rM  rN  r  r  rz  r{  zFailed to download image: HTTP r}  r~  r  r  r  r  r  zimage.rh  rA   ri  Tr`  I[%s] aiohttp not installed, falling back to URL. Run: pip install aiohttpr  z=[%s] Failed to send image attachment, falling back to URL: %sr;   )"r!  r   r&   rx   r   r>   r  
send_imager  r5  r   r6  r  rK  ry  r  r   r  r  r~   r  r  r  rS   rl  r  r7  r[  r/   r>  r(   r   ImportErrorr  )rl   r!  r  rd  r#  r$  r  r   rK  ry  r  r  r  sessionr  
image_datar  r  r  rS  r   r   r>  s                         r5   r_  zDiscordAdapter.send_imagey  s      | 	De?CCCC9%% 	fNNTVZV_```++GY\d+eeeeeeeee=	SNNNl..s7||<<G I $ : :3w<< H HHHHHHH W!%7U'7U7U7UVVVV [ZZZZZZZ&&HHHF 8 8 @ @Hg,w,88x88 L L L L L L LG&7;yee':O:OVX:O:Y:Ye]dee L L L L L L Lim{c))'(W$+(W(WXXX'+yy{{!2!2!2!2!2!2J $(<#3#3NK#P#PLC--,1F1F#,..#<//$III"<

:(>(>RUXXXD,,W55 %)%:%:#%,]$9$9$;$;!% &; & &            )L L L L L L L L L L L L LL L L L L L L L L L L L L L6 !(+2 <! !- ! !      C &ds36{{KKK=L L L L L L L L L L L L LL L L L L L L L L L L L L LL L L L L L L L L L L L L L LL L L L L L L L L L L L L L L L L L L L L L L L L L L L L LB  	S 	S 	SNN[	    
 ++GYRRRRRRRRRRR 	S 	S 	SLLO		     ++GYRRRRRRRRRRRRRR	Ss   8A0K6 )5K6 +K#	C(J>1K#K6 AJ>K#+K6 >
K	K#K	K#K6 #
K--K6 0K-1K6 6AN.	N.AN)#N.)N.animation_urlc                  K   | j         st          dd          S t          |          sLt                              d| j                   t                                          |||||           d{V S 	 ddl}| j         	                    t          |                    }|s-| j                             t          |                     d{V }|st          dd| d	          S dd
lm}m}	  |d          }
 |	|
          \  }} |j        di |4 d{V } |j        |fd|                    d          i|4 d{V }|j        dk    rt'          d|j                   |                                 d{V }ddl}t-          j        |                    |          d          }|                     |          rV|                     ||pd                                |           d{V cddd          d{V  cddd          d{V  S |                    |r|nd|           d{V }t          dt;          |j                            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>          $ rQ t                              d| j        d           t                                          |||||           d{V cY S t&          $ rY}t                               d| j        |d           t                                          |||||           d{V cY d}~S d}~ww xY w)z;Send an animated GIF natively as a Discord file attachment.Fr'  r(  z?[%s] Blocked unsafe animation URL during Discord send_animationr  Nr   r,  r+  rx  rM  rN  r  r  rz  r{  z#Failed to download animation: HTTP zanimation.gifrh  rA   ri  Tr`  r^  r  zA[%s] Failed to send animation attachment, falling back to URL: %sr;   )!r!  r   r&   rx   r   r>   r  send_animationr  r5  r   r6  r  rK  ry  r  r   r  r  r~   r  r  rS   rl  r  r7  r[  r/   r>  r(   r   r`  r  )rl   r!  rc  rd  r#  r$  r  r   rK  ry  r  r  r  ra  r  animation_datar  rS  r   r   r>  s                       r5   re  zDiscordAdapter.send_animation  s      | 	De?CCCC=)) 	nNN\^b^ghhh//QYdl/mmmmmmmmm3	nNNNl..s7||<<G I $ : :3w<< H HHHHHHH W!%7U'7U7U7UVVVV [ZZZZZZZ&&HHHF 8 8 @ @Hg,w,88x88 L L L L L L LG&7;}iig>S>SZ\>S>]>]iahii L L L L L L Lmq{c))'([dk([([\\\+/99;;%6%6%6%6%6%6NIII"<

>(B(B_]]]D,,W55 %)%:%:#%,]$9$9$;$;!% &; & &            L L L L L L L L L L L L LL L L L L L L L L L L L L L" !(+2 <! !- ! !      C &ds36{{KKK)L L L L L L L L L L L L LL L L L L L L L L L L L L LL L L L L L L L L L L L L L LL L L L L L L L L L L L L L L L L L L L L L L L L L L L L L.  	n 	n 	nNN[	    
 //QYdl/mmmmmmmmmmm 	n 	n 	nLLS		     //QYdl/mmmmmmmmmmmmmm	ns   8A0J> )5J> +J+	B0J9J+J> AJ!J+3J> 
J	J+J	J+J> +
J55J> 8J59J> >AM:	M:!AM5/M:5M:
video_pathc                N  K   	 |                      |||           d{V S # t          $ r t          dd|           cY S t          $ rY}t                              d| j        |d           t                                          |||||           d{V cY d}~S d}~ww xY w)	z9Send a local video file natively as a Discord attachment.NFzVideo file not found: r(  zA[%s] Failed to send local video, falling back to base adapter: %sTr  r  )	rn  rZ  r   r~   rx   r  r>   r  
send_video)rl   r!  rg  rd  r#  r$  r   r>  s          r5   ri  zDiscordAdapter.send_video	  s      	g33GZQQQQQQQQQ  	Z 	Z 	Ze3XJ3X3XYYYYYY 	g 	g 	gLL\^b^gijuyLzzz++GZ(]e+ffffffffffffff	gr\  c           	     T  K   	 |                      ||||           d{V S # t          $ r t          dd|           cY S t          $ rZ}t                              d| j        |d           t                                          ||||||	           d{V cY d}~S d}~ww xY w)
z8Send an arbitrary file natively as a Discord attachment.)re  NFzFile not found: r(  z>[%s] Failed to send document, falling back to base adapter: %sTr  r  )	rn  rZ  r   r~   rx   r  r>   r  send_document)	rl   r!  rc  rd  re  r#  r$  r   r>  s	           r5   rk  zDiscordAdapter.send_document	  s      	t33GY[d3eeeeeeeee  	S 	S 	Se3Qi3Q3QRRRRRR 	t 	t 	tLLY[_[dfgrvLwww..w	7IW_jr.ssssssssssssss	ts"   $ B'	B'AB"B'"B'c                    K    j         sdS  j        v rdS d fd}t          j         |                       j        <   dS )a`  Start a persistent typing indicator for a channel.

        Discord's TYPING_START gateway event is unreliable in DMs for bots.
        Instead, start a background loop that hits the typing endpoint every
        8 seconds (typing indicator lasts ~10s).  The loop is cancelled when
        stop_typing() is called (after the response is sent).
        Nr)   r  c                   K   	 	 	 t           j                            dd          } j        j                            |            d {V  nE# t
          j        $ r Y d S t          $ r'}t          	                    d|           Y d }~d S d }~ww xY wt          j
        d           d {V  # t
          j        $ r Y d S w xY w)NTr  z/channels/{channel_id}/typingr  z*Discord typing indicator failed for %s: %s   )rS   r  r  r!  r  r"  r  r~   rx   r   r  )router   r!  rl   s     r5   _typing_loopz0DiscordAdapter.send_typing.<locals>._typing_loop>	  s     +
 ' 2 2"$C'. !3 ! ! #l/77>>>>>>>>>>"1   $   %QSZ\]^^^ "-*********+ )   sG   B/ AA B/ BB/ !	B*BB/ BB/ /CCr)   r  )r!  r6  r"  rX  )rl   r!  r$  rp  s   ``  r5   send_typingzDiscordAdapter.send_typing0	  su       | 	Fd(((F	 	 	 	 	 	 	$ '.&9,,..&I&I7###r7   c                   K   | j                             |d          }|r<|                                 	 | d{V  dS # t          j        t
          f$ r Y dS w xY wdS )z3Stop the persistent typing indicator for a channel.N)r6  r   rW  r"  r  r~   )rl   r!  r  s      r5   stop_typingzDiscordAdapter.stop_typingR	  s      !%%gt44 	KKMMM








*I6   		 	s   ? AAc                  K   | j         sdddS 	 | j                             t          |                    }|s-| j                             t          |                     d{V }|st	          |          ddS t          |t          j                  r%d}|j        r|j        j	        nt	          |          }nt          |t          j
                  r
d}|j	        }n_t          |t          j                  r%d}d|j	         }|j        r|j        j	         d| }n d}t          |d	t	          |                    }||t          |d
          r |j        rt	          |j        j                  ndt          |d
          r|j        r|j        j	        nddS # t           $ rN}t"                              d| j	        ||d           t	          |          dt	          |          dcY d}~S d}~ww xY w)z(Get information about a Discord channel.Unknowndm)r>   rm  NrJ  r   # / r>   ry  )r>   rm  r  
guild_namez'[%s] Failed to get chat info for %s: %sTr  )r>   rm  r  )r!  r5  r   r6  r(   r   rS   rp  	recipientr>   r5  TextChannelry  r:  r   r   r~   rx   r  )rl   r!  r   	chat_typer>   r   s         r5   get_chat_infozDiscordAdapter.get_chat_info\	  s3     | 	5%t444 	Il..s7||<<G I $ : :3w<< H HHHHHHH < #Gd;;; '7#455 > 	181BTw(--GGW^44 
>$	|GW%899 >%	)7<))= <%m0;;T;;D%	wG== !5<Wg5N5NjSZS`jC 0111fj4;GW4M4MiRYR_igm00ei	    	I 	I 	ILLBDIwXYdhLiiiLL$QHHHHHHHH	Is&   A)F :DF 
G. AG)#G.)G.c                  K   | j         r| j        sdS t                      }t                      }| j         D ]S}|                                r|                    |           ,|                    |                                           T|sdS t          d| j         dt          |           dd	                    |                      d}| j        j
        D ]e}	 |j        }t          |          |j        k     r&d |                    d          2              d{V }n8# t          $ r+}t                              d	|j        |           Y d}~zd}~ww xY w|D ]}|j                                        }	|j                                        }
|j        pd
                                }|	|v p|
|v p||v }|rt'          |j                  }|                    |           |dz  }|	|v r|	n|
|v r|
n|}|                    |           t          d| j         d| d| d|j         d|j         d           |s ng|r-t          d| j         dd	                    |                      || _         d	                    t/          |                    t0          j        d<   |rt          d| j         d| d           dS dS )a6  
        Resolve non-numeric entries in DISCORD_ALLOWED_USERS to Discord user IDs.

        Users can specify usernames (e.g. "teknium") or display names instead of
        raw numeric IDs.  After resolution, the env var and internal set are updated
        so authorization checks work with IDs only.
        N[z] Resolving z username(s): , r   c                "   K   g | 3 d {V }|
6 S r   r;   )r   r   s     r5   r   z=DiscordAdapter._resolve_allowed_usernames.<locals>.<listcomp>	  s.      PPPPPPPP1qPPPPs   )limitz(Failed to fetch members for guild %s: %srA   r   z] Resolved 'z' -> rO  rx  )z] Could not resolve usernames: rD  rA  z%] Updated DISCORD_ALLOWED_USERS with z resolved ID(s))r]   r!  rZ   rG  r  r4   printr>   r   r  r#  r   r  fetch_membersr~   rx   r   rz  global_namer(   r   discarddiscriminatorr  rG   environ)rl   numeric_ids
to_resolver'   resolved_country  r   r   r{  
name_lowerdisplay_lowerglobal_lowermatchedr   matched_names                  r5   rU  z)DiscordAdapter._resolve_allowed_usernames	  s[      % 	T\ 	FeeUU
+ 	. 	.E}} .&&&&u{{}}---- 	F_$)__Z__		R\H]H]__```\( 	 	E-w<<%"444PP0C0C$0C0O0OPPPPPPPPPG   I5:WXYYY " v v#[..00
 & 3 9 9 ; ; & 2 8b??AA$
2omz6QoUaeoUo vfi..COOC((("a'N1;z1I1I::)6*)D)D, ! &&|444tditt\ttttv{tt]c]qtttuuu   	YWdiWW		*@U@UWWXXX "-.1hhvk7J7J.K.K
*+ 	gedieeneeefffff	g 	gs   AD##
E-!EEc                    |S )z]
        Format message for Discord.

        Discord uses its own markdown variant.
        r;   )rl   r"  s     r5   r9  zDiscordAdapter.format_message	  s	     r7   discord.Interactionfollowup_msg
str | Nonec                  K   	 |j         }t          |j        dd          pt          |dd          }t                              d|t          |dd          t          |dd          |t          |dd                     n# t
          $ r Y nw xY w|                     ||           d{V sdS |j                            d	           d{V  | 	                    ||          }| 
                    |           d{V  	 |r|                    |
           d{V  dS |                                 d{V  dS # t
          $ r&}t                              d|           Y d}~dS d}~ww xY w)am  Common handler for simple slash commands that dispatch a command string.

        Defers the interaction (shows "thinking..."), dispatches the command,
        then cleans up the deferred response.  If *followup_msg* is provided
        the "thinking..." indicator is replaced with that text; otherwise it
        is deleted so the channel isn't cluttered.
        r   Nr  zA[Discord] slash '%s' invoked by user=%s id=%s channel=%s guild=%sr>   rX  r  TrG  rK  z&Discord interaction cleanup failed: %s)r   r:  r   rx   ry   r~   rE  rI  defer_build_slash_eventhandle_messageedit_original_responsedelete_original_responser   )rl   r*  r@  r  _user_chan_idr  r   s           r5   _run_simple_slashz DiscordAdapter._run_simple_slash	  s     "	$E{2D$??k7;XdfjCkCkHKKSvs++tS))Z66     	 	 	D	
 44[,OOOOOOOO 	F"((4(888888888''\BB!!%(((((((((	F =!888NNNNNNNNNNN!::<<<<<<<<<<< 	F 	F 	FLLA1EEEEEEEEE	Fs0   A:A? ?
BB D<  D< <
E,E''E,c                     j         sdS  j         j        }|                    dd          d fd            }|                    dd	          d fd
            }|                    dd          t          j                            d          dd fd                        }|                    dd          t          j                            d          dd fd                        }|                    dd          t          j                            d          dd fd                        }|                    dd          d fd            }|                    d d!          d fd"            }|                    d#d$          d fd%            }	|                    d&d'          d fd(            }
|                    d)d*          d fd+            }|                    d,d-          t          j                            d./          d fd1                        }|                    d2d3          d fd4            }|                    d5d6          t          j                            d7          dd fd8                        }|                    d9d:          t          j                            d;          dd fd<                        }|                    d=d>          d fd?            }|                    d@dA          d fdB            }|                    dCdD          t          j                            dEF          dd fdJ                        }|                    dKdL          d fdM            }|                    dNdO          d fdP            }|                    dQdR          t          j                            dST          t          j                            t          j                            dUdVW          t          j                            dXdYW          t          j                            dZd[W          t          j                            d\d]W          t          j                            d^d_W          t          j                            d`d#W          gT          dd fdb                                    }|                    dcdd          d fde            }|                    dfdg          d fdh            }|                    didj          t          j                            dkl          dd fdn                        }|                    dodp          t          j                            dql          dd fdr                        }|                    dsdt          t          j                            dudvdwx          	 	 dd fd|                        }|                    d}d~          t          j                            d/          d fd                        }|                    dd          t          j                            d/          d fd                        }dd fd}t                      }	 ddl	m
}m} m}! 	 d |                                D             }n# t          $ r Y nw xY w |!            }"|D ]}# | |#|"          s|#j                                        dd         }$|$|v r5 ||#j        |#j        |#j                  }%	 |                    |%           |                    |$           }# t          $ r Y w xY wt*                              dt/          |                     n2# t          $ r%}&t*                              d|&           Y d}&~&nd}&~&ww xY w	 ddl	m}'  |'            D ]o\  }(})}*|(                                dd         }$|$|v r' ||(|)|*          }%	 |                    |%           |                    |$           `# t          $ r Y lw xY wn2# t          $ r%}&t*                              d|&           Y d}&~&nd}&~&ww xY w                     |           t7          j        dd                                                                          dv r                     |           dS dS )z4Register Discord slash commands on the command tree.NnewzStart a new conversation)r>   r  r*  r  c                F   K                        | dd           d {V  d S )N/resetzNew conversation started~r  r*  rl   s    r5   	slash_newz:DiscordAdapter._register_slash_commands.<locals>.slash_new
  s8      ((h@[\\\\\\\\\\\r7   resetzReset your Hermes sessionc                F   K                        | dd           d {V  d S )Nr  zSession reset~r  r  s    r5   slash_resetz<DiscordAdapter._register_slash_commands.<locals>.slash_reset
  s8      ((h@PQQQQQQQQQQQr7   modelzShow or change the modelzHModel name (e.g. anthropic/claude-sonnet-4). Leave empty to see current.r>   rA   r>   r(   c                n   K                        | d|                                            d {V  d S )Nz/model r  r/   r*  r>   rl   s     r5   slash_modelz<DiscordAdapter._register_slash_commands.<locals>.slash_model

  L       ((6F6F6F6L6L6N6NOOOOOOOOOOOr7   	reasoningzShow or change reasoning effortz=Reasoning effort: none, minimal, low, medium, high, or xhigh.)effortr  c                n   K                        | d|                                            d {V  d S )Nz/reasoning r  )r*  r  rl   s     r5   slash_reasoningz@DiscordAdapter._register_slash_commands.<locals>.slash_reasoning
  sL       ((6LF6L6L6R6R6T6TUUUUUUUUUUUr7   personalityzSet a personalityz0Personality name. Leave empty to list available.c                n   K                        | d|                                            d {V  d S )Nz/personality r  r  s     r5   slash_personalityzBDiscordAdapter._register_slash_commands.<locals>.slash_personality
  sL       ((6Ld6L6L6R6R6T6TUUUUUUUUUUUr7   retryzRetry your last messagec                F   K                        | dd           d {V  d S )Nz/retryz	Retrying~r  r  s    r5   slash_retryz<DiscordAdapter._register_slash_commands.<locals>.slash_retry
  s7      ((hLLLLLLLLLLLr7   undozRemove the last exchangec                D   K                        | d           d {V  d S )Nz/undor  r  s    r5   
slash_undoz;DiscordAdapter._register_slash_commands.<locals>.slash_undo
  5      ((g>>>>>>>>>>>r7   r  zShow Hermes session statusc                F   K                        | dd           d {V  d S )Nz/statuszStatus sent~r  r  s    r5   slash_statusz=DiscordAdapter._register_slash_commands.<locals>.slash_status!
  s7      ((iPPPPPPPPPPPr7   sethomez!Set this chat as the home channelc                D   K                        | d           d {V  d S )Nz/sethomer  r  s    r5   slash_sethomez>DiscordAdapter._register_slash_commands.<locals>.slash_sethome%
  s5      ((jAAAAAAAAAAAr7   r   zStop the running Hermes agentc                F   K                        | dd           d {V  d S )Nz/stopzStop requested~r  r  s    r5   
slash_stopz;DiscordAdapter._register_slash_commands.<locals>.slash_stop)
  s8      ((g?PQQQQQQQQQQQr7   steerz8Inject a message after the next tool call (no interrupt)z0Text to inject into the agent's next tool result)promptr  c                n   K                        | d|                                            d {V  d S )Nz/steer r  r*  r  rl   s     r5   slash_steerz<DiscordAdapter._register_slash_commands.<locals>.slash_steer-
  sL       ((6H6H6H6N6N6P6PQQQQQQQQQQQr7   compresszCompress conversation contextc                D   K                        | d           d {V  d S )Nz	/compressr  r  s    r5   slash_compressz?DiscordAdapter._register_slash_commands.<locals>.slash_compress2
  s5      ((kBBBBBBBBBBBr7   titlezSet or show the session titlez+Session title. Leave empty to show current.c                n   K                        | d|                                            d {V  d S )Nz/title r  r  s     r5   slash_titlez<DiscordAdapter._register_slash_commands.<locals>.slash_title6
  r  r7   r   z!Resume a previously-named sessionz5Session name to resume. Leave empty to list sessions.c                n   K                        | d|                                            d {V  d S )Nz/resume r  r  s     r5   slash_resumez=DiscordAdapter._register_slash_commands.<locals>.slash_resume;
  sL       ((6G6G6G6M6M6O6OPPPPPPPPPPPr7   usagez!Show token usage for this sessionc                D   K                        | d           d {V  d S )Nz/usager  r  s    r5   slash_usagez<DiscordAdapter._register_slash_commands.<locals>.slash_usage@
  s5      ((h???????????r7   helpzShow available commandsc                D   K                        | d           d {V  d S )Nz/helpr  r  s    r5   
slash_helpz;DiscordAdapter._register_slash_commands.<locals>.slash_helpD
  r  r7   insightsz!Show usage insights and analyticsz&Number of days to analyze (default: 7))daysrw  r  r   c                J   K                        | d|            d {V  d S )Nz
/insights r  )r*  r  rl   s     r5   slash_insightsz?DiscordAdapter._register_slash_commands.<locals>.slash_insightsH
  s@       ((6I46I6IJJJJJJJJJJJr7   z
reload-mcpzReload MCP servers from configc                D   K                        | d           d {V  d S )Nz/reload-mcpr  r  s    r5   slash_reload_mcpzADiscordAdapter._register_slash_commands.<locals>.slash_reload_mcpM
  s5      ((mDDDDDDDDDDDr7   zreload-skillsz3Re-scan ~/.hermes/skills/ for new or removed skillsc                D   K                        | d           d {V  d S )Nz/reload-skillsr  r  s    r5   slash_reload_skillszDDiscordAdapter._register_slash_commands.<locals>.slash_reload_skillsQ
  s6      ((6FGGGGGGGGGGGr7   r  zToggle voice reply modez3Voice mode: on, off, tts, channel, leave, or status)modeu#   channel — join your voice channelr   r  u   leave — leave voice channelleaveu$   on — voice reply to voice messagesrF   u#   tts — voice reply to all messagesttsu   off — text onlyr   u   status — show current moder  c                n   K                        | d|                                            d {V  d S )Nz/voice r  )r*  r  rl   s     r5   slash_voicez<DiscordAdapter._register_slash_commands.<locals>.slash_voiceU
  sL       ((6F6F6F6L6L6N6NOOOOOOOOOOOr7   updatez)Update Hermes Agent to the latest versionc                F   K                        | dd           d {V  d S )Nz/updatezUpdate initiated~r  r  s    r5   slash_updatez=DiscordAdapter._register_slash_commands.<locals>.slash_updateb
  s8      ((iATUUUUUUUUUUUr7   restartz%Gracefully restart the Hermes gatewayc                F   K                        | dd           d {V  d S )Nz/restartzRestart requested~r  r  s    r5   slash_restartz>DiscordAdapter._register_slash_commands.<locals>.slash_restartf
  s8      ((jBVWWWWWWWWWWWr7   approvez#Approve a pending dangerous commandzAOptional: 'all', 'session', 'always', 'all session', 'all always')scoper  c                n   K                        | d|                                            d {V  d S )Nz	/approve r  r*  r  rl   s     r5   slash_approvez>DiscordAdapter._register_slash_commands.<locals>.slash_approvej
  sL       ((6I%6I6I6O6O6Q6QRRRRRRRRRRRr7   denyz Deny a pending dangerous commandz,Optional: 'all' to deny all pending commandsc                n   K                        | d|                                            d {V  d S )Nz/deny r  r  s     r5   
slash_denyz;DiscordAdapter._register_slash_commands.<locals>.slash_denyo
  sL       ((6Fu6F6F6L6L6N6NOOOOOOOOOOOr7   rJ  z4Create a new thread and start a Hermes session in itzThread namez6Optional first message to send to Hermes in the threadz/Auto-archive in minutes (60, 1440, 4320, 10080)r>   r\  auto_archive_durationr   r\  r  c                H   K                        | |||           d {V  d S r   )_handle_thread_create_slash)r*  r>   r\  r  rl   s       r5   slash_threadz=DiscordAdapter._register_slash_commands.<locals>.slash_threadt
  s<       22;gOdeeeeeeeeeeer7   queuez4Queue a prompt for the next turn (doesn't interrupt)zThe prompt to queuec                L   K                        | d| d           d {V  d S )Nz/queue zQueued for the next turn.r  r  s     r5   slash_queuez<DiscordAdapter._register_slash_commands.<locals>.slash_queue
  sC       ((6H6H6HJefffffffffffr7   
backgroundzRun a prompt in the backgroundz#The prompt to run in the backgroundc                L   K                        | d| d           d {V  d S )Nz/background zBackground task started~r  r  s     r5   slash_backgroundzADiscordAdapter._register_slash_commands.<locals>.slash_background
  sC       ((6MV6M6MOijjjjjjjjjjjr7   _name_description
_args_hintc                   |                                  dd         }|pd|  dd         }t          |          }|rd	fd} || |          }nd	fd	} ||           }t          j                            |||
          S )zGBuild a discord.app_commands.Command that proxies to _run_simple_slash.Nr   zRun /r  _DiscordAdapter__namer(   _DiscordAdapter__hintc                     t           j                            d| d d                   dd fd	            }d
                     dd           |_        |S )NzArguments: r  )argsrA   r*  r  r  r(   c                t   K                        | d d|                                            d {V  d S )N/ r  )r*  r  r  rl   s     r5   _handlerzxDiscordAdapter._register_slash_commands.<locals>._build_auto_slash_command.<locals>._make_args_handler.<locals>._handler
  si      "44')<V)<)<d)<)<)B)B)D)D          r7   auto_slash_-r   rA   )r*  r  r  r(   )rS   app_commandsdescribereplacer  )r  r  r
  rl   s   `  r5   _make_args_handlerzfDiscordAdapter._register_slash_commands.<locals>._build_auto_slash_command.<locals>._make_args_handler
  s    )228Nf8N8NtPSt8T2UU       VU )QfnnS#6N6N(P(PH%#Or7   c                T     d fd}d                      dd           |_        |S )Nr*  r  c                J   K                        | d            d {V  d S )Nr  r  )r*  r  rl   s    r5   r
  zzDiscordAdapter._register_slash_commands.<locals>._build_auto_slash_command.<locals>._make_simple_handler.<locals>._handler
  s;      "44[,f,,OOOOOOOOOOOr7   r  r  r   r*  r  )r  r  )r  r
  rl   s   ` r5   _make_simple_handlerzhDiscordAdapter._register_slash_commands.<locals>._build_auto_slash_command.<locals>._make_simple_handler
  sQ    P P P P P P P(PfnnS#6N6N(P(PH%#Or7   r>   r  callback)r  r(   r  r(   )r  r(   )r4   r8   rS   r  Command)
r  r   r  discord_namedeschas_argsr  handlerr  rl   s
            r5   _build_auto_slash_commandzJDiscordAdapter._register_slash_commands.<locals>._build_auto_slash_command
  s     ;;=="-L 3OEOOTcT:DJ''H 6$ $ $ $ $ $ -,UJ??$ $ $ $ $ $ /.u55'//!   0   r7   r   )COMMAND_REGISTRY_is_gateway_available_resolve_config_gatesc                    h | ]	}|j         
S r;   r  r   cmds     r5   rC  z:DiscordAdapter._register_slash_commands.<locals>.<setcomp>
  s    %N%N%N3ch%N%N%Nr7   r   z9Discord auto-registered %d commands from COMMAND_REGISTRYz6Discord auto-register from COMMAND_REGISTRY failed: %s)_iter_plugin_command_entriesz5Discord auto-register from plugin commands failed: %sDISCORD_HIDE_SLASH_COMMANDSr  rB   r  r  )r*  r  r>   r(   )r*  r  r  r(   )r*  r  r  r(   )rw  )r*  r  r  r   )r*  r  r  r(   )r*  r  r  r(   rA   r   )r*  r  r>   r(   r\  r(   r  r   )r  r(   r   r(   r  r(   )r!  r  r  rS   r  r  r  ChoicerZ   hermes_cli.commandsr  r  r   r  r~   r>   r4   r  	args_hintadd_commandr  rx   r   r   r   r$  _register_skill_grouprG   rH   r/   _apply_owner_only_visibility)+rl   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  already_registeredr  r  r   config_overridescmd_defr  auto_cmdr   r$  plugin_nameplugin_descplugin_args_hints+   `                                          r5   r  z'DiscordAdapter._register_slash_commands	  sn   | 	F| 	5.H	I	I	] 	] 	] 	] 	] 
J	I	] 
70K	L	L	R 	R 	R 	R 	R 
M	L	R 
70J	K	K			&	&,v	&	w	w	P 	P 	P 	P 	P 	P 
x	w 
L	K	P 
;4U	V	V			&	&.m	&	n	n	V 	V 	V 	V 	V 	V 
o	n 
W	V	V 
=6I	J	J			&	&,^	&	_	_	V 	V 	V 	V 	V 	V 
`	_ 
K	J	V 
70I	J	J	M 	M 	M 	M 	M 
K	J	M 
6/I	J	J	? 	? 	? 	? 	? 
K	J	? 
81M	N	N	Q 	Q 	Q 	Q 	Q 
O	N	Q 
92U	V	V	B 	B 	B 	B 	B 
W	V	B 
6/N	O	O	R 	R 	R 	R 	R 
P	O	R 
70j	k	k			&	&.`	&	a	a	R 	R 	R 	R 	R 
b	a 
l	k	R 
:3R	S	S	C 	C 	C 	C 	C 
T	S	C 
70O	P	P			&	&,Y	&	Z	Z	P 	P 	P 	P 	P 	P 
[	Z 
Q	P	P 
81T	U	U			&	&,c	&	d	d	Q 	Q 	Q 	Q 	Q 	Q 
e	d 
V	U	Q 
70S	T	T	@ 	@ 	@ 	@ 	@ 
U	T	@ 
6/H	I	I	? 	? 	? 	? 	? 
J	I	? 
:3V	W	W			&	&,T	&	U	U	K 	K 	K 	K 	K 	K 
V	U 
X	W	K 
<5U	V	V	E 	E 	E 	E 	E 
W	V	E 
?8m	n	n	H 	H 	H 	H 	H 
o	n	H 
70I	J	J			&	&,a	&	b	b			%	% ''-RZc'dd ''-LT['\\ ''-S[_'`` ''-RZ_'`` ''-@'NN ''-KS['\\,
	% 
 
	P 	P 	P 	P 	P 	P
 
 
c	b 
K	J	P 
81\	]	]	V 	V 	V 	V 	V 
^	]	V 
92Y	Z	Z	X 	X 	X 	X 	X 
[	Z	X 
92W	X	X			&	&-p	&	q	q	S 	S 	S 	S 	S 	S 
r	q 
Y	X	S 
6/Q	R	R			&	&-[	&	\	\	P 	P 	P 	P 	P 	P 
]	\ 
S	R	P 
81g	h	h			&	&L"S 
' 

 

 )-		f 	f 	f 	f 	f 	f

 

 
i	h	f 
70f	g	g			&	&.C	&	D	D	g 	g 	g 	g 	g 
E	D 
h	g	g 
<5U	V	V			&	&.S	&	T	T	k 	k 	k 	k 	k 
U	T 
W	V	k	 	 	 	 	 	 	@ (+uu#	Xjjjjjjjjjj%N%N$:K:K:M:M%N%N%N""     5466+  ,,W6FGG &|1133CRC8#55544L'% 
$$X...&**<8888     D
 LLK&''     	X 	X 	XNNSUVWWWWWWWW	X	HHHHHH>Z>Z>\>\  :[*:*00223B37#55544$ 
$$X...&**<8888     D   	 	 	NNG       	 	""4((( 92G<<BBDDJJLL Q
 
 
 --d33333
 
s    
^< [* )^< *
[74^< 6[77A ^< *^^< 
^^< ^+^< <
_+_&&_+/Aa0 4*aa0 
a,)a0 +a,,a0 0
b:bbc           
        	 t          j        d          }n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY wd}|                                D ]Q}	 ||_        |dz  }# t          $ r5}t                              dt          |dd          |           Y d}~Jd}~ww xY wt          	                    d|           dS )	ui  Set default_member_permissions=0 on every registered slash command.

        Discord interprets ``Permissions(0)`` as "requires no permissions",
        which paradoxically means the command is hidden from every guild
        member except those with the Administrator permission. Server admins
        can re-grant per user/role via Server Settings → Integrations →
        <bot> → Permissions.

        Authoritative gate is ``_check_slash_authorization`` on every
        invocation, which catches stale clients, role grants made by
        mistake, and direct API calls bypassing Discord's UI hide.
        r   zG[Discord] _apply_owner_only_visibility: cannot build Permissions(0): %sNr   z5[Discord] Could not set default_permissions on %r: %sr>   rX  zy[Discord] Hid %d slash command(s) from non-admin guild members (opt-in defense in depth via DISCORD_HIDE_SLASH_COMMANDS).)
rS   Permissionsr~   rx   r   r  r  r   r:  ry   )rl   r  no_permsr   appliedr#  s         r5   r,  z+DiscordAdapter._apply_owner_only_visibility  s*   	*1--HH 	 	 	NNY   FFFFF	 $$&& 	 	C*2'1   KC--q       
 	I	
 	
 	
 	
 	
s,    
AAA#A00
B/:+B**B/c                ^    	 t                      }	 d |                                D             }n# t          $ r Y nw xY wg  _        i  _        t          |           _                                           j        sdS d fd	}t          j        	                    d
d          t          j        
                    |          	 dd fd                        }t          j                            dd|          }|                    |           t                              d j        t!           j                              j        r(t                              d j         j                   dS dS # t          $ r,}t                              d j        |           Y d}~dS d}~ww xY w)uQ  Register a single ``/skill`` command with autocomplete on the name.

        Discord enforces an ~8000-byte per-command payload limit. The older
        nested layout (``/skill <category> <name>``) registered one giant
        command whose serialized payload grew linearly with the skill
        catalog — with the default ~75 skills the payload was ~14 KB and
        ``tree.sync()`` rejected the entire slash-command batch (issues
        #11321, #10259, #11385, #10261, #10214).

        Autocomplete options are fetched dynamically by Discord when the
        user types — they do NOT count against the per-command registration
        budget. So we register ONE flat ``/skill`` command with
        ``name: str`` (autocompleted) and ``args: str = ""``. This scales
        to thousands of skills with no size math, no splitting, and no
        hidden skills. The slash picker also becomes more discoverable —
        Discord live-filters by the user's typed prefix against both the
        skill name and its description.

        The entries list and lookup dict are stored on ``self`` rather
        than captured in closure variables so :meth:`refresh_skill_group`
        can repopulate them when the user runs ``/reload-skills`` without
        needing to touch the Discord slash-command tree or trigger a
        ``tree.sync()`` call.
        c                    h | ]	}|j         
S r;   r  r"  s     r5   rC  z7DiscordAdapter._register_skill_group.<locals>.<setcomp>F  s    !J!J!Js#(!J!J!Jr7   Nr*  r+  r  r(   r)   r   c                &  K   	 
                     |           \  }}n# t          $ r g cY S w xY w|sg S |pd                                                                }g }
j        D ]\  }}}|r.||                                v s|r||                                v ru|r| d| }	n|}	t          |	          dk    r|	dd         dz   }	|                    t          j        	                    |	|                     t          |          dk    r n|S )	u  Filter skills by the user's typed prefix.

                Matches both the skill name and its description so
                "/skill pdf" surfaces skills whose description mentions
                PDFs even if the name doesn't. Discord caps this list at
                25 entries per query.

                Authorization: a quiet pre-check evaluates the slash
                allowlists and returns ``[]`` for unauthorized users so
                the installed skill catalog is not leaked to anyone who
                can see the command in the picker. Returning a generic
                empty list here is intentional — sending a per-keystroke
                ephemeral rejection would produce a barrage of error
                popups during typing.

                Reads ``self._skill_entries`` so a ``/reload-skills`` run
                since process start shows up on the very next keystroke.
                rA   r  r  Na   r_  r     )
r?  r~   r/   r4   _skill_entriesr   r   rS   r  r'  )r*  r  r   _reasonqr  r>   r  _keylabelrl   s             r5   _autocomplete_namez@DiscordAdapter._register_skill_group.<locals>._autocomplete_nameV  s]     *'+'I'I+'V'V$GWW     III  I]))++1133 "(,(; " "$D$ "TZZ\\ 1 1d 1qDJJLL?P?P )'+$8$8$$8$8EE$(Eu::++$)#2#J$6E#077U$7OO   w<<2--!Es    --zWhich skill to runz Optional arguments for the skill)r>   r  r  rA   r>   r  c                @  K                        | d           d {V sd S j                            |          }|s(| j                            d| dd           d {V  d S |\  }}                    | | d|                                            d {V  d S )Nz/skillzUnknown skill: `z-`. Start typing for autocomplete suggestions.TrG  r	  )rE  _skill_lookupr   rI  rJ  r  r/   )r*  r>   r  r'   _desccmd_keyrl   s         r5   _skill_handlerz<DiscordAdapter._register_skill_group.<locals>._skill_handler  s      "<<[(SSSSSSSS F*..t44 %.;;54 5 5 5"& <         
 F!&w,,G!4!4d!4!4!:!:!<!<          r7   skillzRun a Hermes skillr  z@[%s] Registered /skill command with %d skill(s) via autocompletez?[%s] %d skill(s) filtered out of /skill (name clamp / reserved)z*[%s] Failed to register /skill command: %s)r*  r+  r  r(   r)   r   r  )r*  r+  r>   r(   r  r(   )rZ   r  r~   r=  rD  _skill_group_reserved_names_refresh_skill_catalog_staterS   r  r  r  r  r*  rx   ry   r>   r   _skill_group_hidden_countr   )rl   r  existing_namesrB  rG  r#  excs   `      r5   r+  z$DiscordAdapter._register_skill_group*  s1   2n	Y UUN!J!Jd6G6G6I6I!J!J!J    ?AD=?D9<^9L9LD,--///& - - - - - -^ !**)7 +   !..4F.GGKM      HG	 
, &..0' /  C
 S!!!KKR	3t233   - UIt=     
  	Y 	Y 	YNNGTWXXXXXXXXX	Ys:   E6 1 E6 
>E6 >A E6  C2E6 6
F, !F''F,c                h   ddl m} t          | dt                                } |t          |                    \  }}}t	          |          }|                                D ]}|                    |           |                    d            || _        d |D             | _	        || _
        dS )	u  Re-scan disk for skills and repopulate ``self._skill_entries``.

        Called once from :meth:`_register_skill_group` at startup and
        again from :meth:`refresh_skill_group` whenever the user runs
        ``/reload-skills``. No Discord API calls are made — autocomplete
        and the handler both read from these instance attributes
        directly, so an in-place mutation is sufficient.
        r   )"discord_skill_commands_by_categoryrI  )reserved_namesc                    | d         S )Nr   r;   )ts    r5   <lambda>z=DiscordAdapter._refresh_skill_catalog_state.<locals>.<lambda>  s
    1Q4 r7   )r  c                     i | ]\  }}}|||fS r;   r;   )r   nr   ks       r5   r  z?DiscordAdapter._refresh_skill_catalog_state.<locals>.<dictcomp>  s$    ???GAq!a!Q???r7   N)r(  rO  r:  rZ   r   r  r   sortr=  rD  rK  )rl   rO  reserved
categoriesuncategorizedhiddenentries
cat_skillss           r5   rJ  z+DiscordAdapter._refresh_skill_catalog_state  s     	KJJJJJ4!>FF,N,Nx==-
 -
 -
)
M6 /3=.A.A$++-- 	' 	'JNN:&&&& 	(((%??w???)/&&&r7   tuple[int, int]c                   	 |                                   nX# t          $ rK}t                              d| j        |           t          t          | dg                     dfcY d}~S d}~ww xY wt                              d| j        t          | j                  | j	                   t          | j                  | j	        fS )u  Rescan skills and update the live ``/skill`` autocomplete state.

        Invoked by :meth:`gateway.run.GatewayOrchestrator._handle_reload_skills_command`
        after :func:`agent.skill_commands.reload_skills` has refreshed
        the in-process skill-command registry. Without this call, the
        ``/skill`` autocomplete dropdown keeps showing the list captured
        at process start — new skills stay invisible and deleted skills
        return an "Unknown skill" error when clicked.

        Because autocomplete options are fetched dynamically by Discord,
        we only need to mutate the entries/lookup attributes read by the
        callbacks — no ``tree.sync()`` is required.

        Returns ``(new_count, hidden_count)``.
        z;[%s] Failed to refresh /skill autocomplete after reload: %sr=  r   NzG[%s] Refreshed /skill autocomplete: %d skill(s) available (%d filtered))
rJ  r~   rx   r   r>   r   r:  ry   r=  rK  )rl   rM  s     r5   refresh_skill_groupz"DiscordAdapter.refresh_skill_group  s     	A--//// 	A 	A 	ANNM	3   &6;;<<a@@@@@@@	A 	UI#$$*		
 	
 	
 D'(($*HIIs    
A,A A'!A,'A,textc           
        t          |j        t          j                  }t          |j        t          j                  }d}|rd}n|rd}t          |j                  }nd}d}|sXt          |j        d          rC|j        j        }t          |j        d          r"|j        j	        r|j        j	        j         d| }| 
                    |j        |	          }|                     t          |j                  ||t          |j        j                  |j        j        ||
          }	|                    d          rt           j        nt           j        }
t          |j                  }t          t'          t'          |dd          dd          pd          }t)          ||
|	||                     ||pd                    S )z>Build a MessageEvent from a Discord slash command interaction.Nrw  rJ  grouprA   r>   ry   / #	is_threadr!  	chat_namer}  r   rL  r*  
chat_topicr  r   r;  )ra  message_typer  r  channel_prompt)r   r   rS   rp  r5  r(   r  r   r>   ry  _get_effective_topicbuild_sourcer   r   rz  r0   r   COMMANDTEXTr:  r   _resolve_channel_prompt)rl   r*  ra  is_dmrf  r*  r}  rh  ri  r  msg_typer  r;  s                r5   r  z!DiscordAdapter._build_slash_event  s   ;.0ABB{2GNCC		 	 II 	  IK233III	 	O!4f== 	O#+0I{*G44 O9L9R O*28=NN9NN	 ..{/Bi.XX
"".//(+,,!&3! # 
 
 +///#*>*>T;&&KDT/00
Y E E{TVWW][]^^	!#77
IDUQUVV
 
 
 	
r7   r   r>   r  c                  K   |                      |d           d{V sdS |j                            d           d{V  |                     ||||           d{V }|                    d          s=|                    dd          }|j                            d	| d           d{V  dS |                    d
          }|                    d          p|}|rd| dnd| d}	|j                            d|	 d           d{V  |r| j                            |           |pd	                                }
|
r"|r"| 
                    ||||
           d{V  dS dS dS )zGCreate a Discord thread from a slash command and start a session in it.z/threadNTrG  r  r)  r  zunknown errorzFailed to create thread: r*  rO  z<#r,   **zCreated thread rA   )rE  rI  r  _create_threadr   followupr>  r5  markr/   _dispatch_thread_session)rl   r*  r>   r\  r  r  r  r*  rO  linkstarters              r5   r  z*DiscordAdapter._handle_thread_create_slash  s'      44[)LLLLLLLL 	F"((4(888888888**"7	 + 
 
 
 
 
 
 
 
 zz)$$ 	JJw88E&++,O,O,O[_+`````````FJJ{++	jj//74 %.G I    3G3G3G3G"''(@$(@(@D'QQQQQQQQQ  	*My))) =b'')) 	^y 	^//YU\]]]]]]]]]]]	^ 	^ 	^ 	^r7   r*  c           	       K   d}t          |d          r|j        r|j        j        }|r| d| n|}t          |dd          }|r|                     |d          nd}|                     ||dt          |j        j                  |j        j	        ||	          }	| 
                    t          |dd                    }
t          t          |
d
d          pd          }|                     ||pd          }|                     ||pd          }t          |t          j        |	|||          }|                     |           d{V  dS )zMBuild a MessageEvent pointing at a thread and send it through handle_message.rA   ry  ry  r   NTre  rJ  rg  r   )ra  rj  r  r  
auto_skillrk  )r   ry  r>   r:  rl  rm  r(   r   r   rz  _thread_parent_channel_resolve_channel_skillsrp  r   r   ro  r  )rl   r*  r*  rO  ra  rz  rh  _chanri  r  _parent_channel
_parent_id_skills_channel_promptr  s                  r5   rx  z'DiscordAdapter._dispatch_thread_sessionE  s      
;(( 	0[-> 	0$*/J7ARz33k333{	 Y55INXT..u.EEETX
""(+,,!&3! # 
 
 55gk9VZ6[6[\\$;;ArBB
..y*:LMM66y*BTPTUU$)#*
 
 
 !!%(((((((((((r7   r  r;  list[str] | Nonec                <    ddl m}  || j        j        ||          S )a1  Look up auto-skill bindings for a Discord channel/forum thread.

        Config format (in platform extra):
            channel_skill_bindings:
              - id: "123456"
                skills: ["skill-a", "skill-b"]
        Also checks parent_id so forum threads inherit the forum's bindings.
        r   )resolve_channel_skills)r  r  r  r<  )rl   r  r;  r  s       r5   r~  z&DiscordAdapter._resolve_channel_skillso  s1     	BAAAAA%%dk&7YOOOr7   c                <    ddl m}  || j        j        ||          S )zSResolve a Discord per-channel prompt, preferring the exact channel over its parent.r   )resolve_channel_prompt)r  r  r  r<  )rl   r  r;  r  s       r5   rp  z&DiscordAdapter._resolve_channel_prompt{  s.    AAAAAA%%dk&7YOOOr7   c                
   | j         j                            d          }|:t          |t                    r|                                dvS t          |          S t          j        dd                                          dvS )z>Return whether Discord channel messages require a bot mention.require_mentionN)r  r  r  r   DISCORD_REQUIRE_MENTIONrC   )	r  r<  r   r   r(   r4   r8   rG   rH   )rl   
configureds     r5   _discord_require_mentionz'DiscordAdapter._discord_require_mention  s    [&**+<==
!*c** M!''))1LLL
###y2F;;AACCKfffr7   rZ   c                X   | j         j                            d          }|t          j        dd          }t          |t                    rd |D             S |!t          |                                          nd}|rd |	                    d          D             S t                      S )a"  Return Discord channel IDs where no bot mention is required.

        A single ``"*"`` entry (either from a list or a comma-separated
        string) is preserved in the returned set so callers can short-circuit
        on wildcard membership, consistent with ``allowed_channels``.
        free_response_channelsNDISCORD_FREE_RESPONSE_CHANNELSrA   c                    h | ]D}t          |                                          #t          |                                          ES r;   )r(   r/   r   parts     r5   rC  zADiscordAdapter._discord_free_response_channels.<locals>.<setcomp>  s=    KKK$T9J9JKCIIOO%%KKKr7   c                ^    h | ]*}|                                 |                                 +S r;   r0  r  s     r5   rC  zADiscordAdapter._discord_free_response_channels.<locals>.<setcomp>  s-    JJJTTZZ\\JDJJLLJJJr7   rD  )r  r<  r   rG   rH   r   r   r(   r/   r  rZ   )rl   rI   ss      r5   _discord_free_response_channelsz.DiscordAdapter._discord_free_response_channels  s     k##$<==;)<bAACc4   	LKK#KKKK !$CHHNNR 	KJJQWWS\\JJJJuur7   r   c                (    t          |dd          p|S )z:Return the parent text channel when invoked from a thread.parentNr  )rl   r   s     r5   r}  z%DiscordAdapter._thread_parent_channel  s    w$//:7:r7   Optional[Any]c                @  K   t          |dd          }||S | j        sdS t          |dd          }|dS | j                            t          |                    }||S 	 | j                            t          |                     d{V S # t
          $ r Y dS w xY w)zFReturn the interaction channel, fetching it if the payload is partial.r   Nr  )r:  r!  r5  r   r6  r~   )rl   r*  r   r  s       r5   _resolve_interaction_channelz+DiscordAdapter._resolve_interaction_channel  s      +y$77N| 	4[,==
4,**3z??;;N	33C
OODDDDDDDDD 	 	 	44	s   ",B 
BB)r\  r  c                 K   |pd                                 }|sddiS |t          vr9d                    d t          t                    D                       }dd| diS |                     |           d{V }|dd	iS t          |t          j                  rdd
iS |                     |          }|ddiS t          t          |dd          dd          pd}d| d}	|pd                                 }
	 |
                    |||	           d{V }|
r|                    |
           d{V  dt          |j                  t          |dd          p|dS # t          $ r}	 |
pd| d}|                    |           d{V }|
                    |||	           d{V }dt          |j                  t          |dd          p|dcY d}~S # t          $ r}dd| d| icY d}~cY d}~S d}~ww xY wd}~ww xY w)zCreate a thread in the current Discord channel.

        Tries ``parent_channel.create_thread()`` first.  If Discord rejects
        that (e.g. permission issues), falls back to sending a seed message
        and creating the thread from it.
        rA   r  zThread name is required.r  c              3  4   K   | ]}t          |          V  d S r   r  )r   vs     r5   rJ  z0DiscordAdapter._create_thread.<locals>.<genexpr>  s(      ZZ1AZZZZZZr7   z&auto_archive_duration must be one of: r  Nz.Could not resolve the current Discord channel.zIDiscord threads can only be created inside server text channels, not DMs.z=Could not determine a parent text channel for the new thread.r   rz  unknown userzRequested by z via /threadr>   r  rC  Tr>   )r)  r*  rO  !   🧵 Thread created by Hermes: **rt  zTDiscord rejected direct thread creation and the fallback also failed. Direct error: z. Fallback error: )r/   !VALID_THREAD_AUTO_ARCHIVE_MINUTESr  r  r  r   rS   rp  r}  r:  rN  r>  r(   r   r~   )rl   r*  r>   r\  r  r   r   parent_channelrz  rC  starter_messagerJ  direct_errorseed_contentseed_msgfallback_errors                   r5   ru  zDiscordAdapter._create_thread  s[      
!!## 	9788 (IIIiiZZ7X0Y0YZZZZZGPgPPPQQ99+FFFFFFFF?MNNgw011 	jhii44W==!\]]w{FDAA>SWXXj\j;;;;"=b//11!	)77&; 8        F
  3kk/222222222 ^^&vvt<<D  
  	 	 	.d2d\`2d2d2d!/!4!4\!B!BBBBBBB'55*?!  6            $!$VY#*664#@#@#HD       
    Z)5Z ZIWZ Z           	sJ   :A#E 
H)A)GH
G;"	G6+G;,G>0H6G;;G>>H'DiscordMessage'c                  K   |j         pd                                }t          j        dd|          }t          j        dd|          }t          j        dd|                                          }|r
|dd         nd}t	          |          dk    r|dd	         d
z   }	 |                    |d           d{V }|S # t          $ r}t          t          |dd          dd          pd}d| }	 |j        	                    d| d           d{V }|                    |d|           d{V }|cY d}~S # t          $ r1}	t                              d| j        ||	           Y d}	~	Y d}~dS d}	~	ww xY wd}~ww xY w)zCreate a thread from a user message for auto-threading.

        Returns the created thread object, or ``None`` on failure.
        rA   z<@[!&]?\d+>z<#\d+>z\s+r	  Nr|  HermesM   r_  r   )r>   r  rl  rz  r  zAuto-threaded from mention by r  rt  r  zF[%s] Auto-thread creation failed. Direct error: %s. Fallback error: %s)r"  r/   resubr   rN  r~   r:  r   r>  rx   r   r>   )
rl   r\  r"  rO  rJ  r  rz  rC  r  r  s
             r5   _auto_create_threadz"DiscordAdapter._auto_create_thread  s	      ?(b//11&W55&B00&g..4466&-;gcrcll8w<<"%crc*U2K	"00kY]0^^^^^^^^FM 	 	 	"77Hd#C#C^UYZZl^lLDlDDF!(!5!56o`k6o6o6o!p!ppppppp'55$*.!  6          
    \I "	   ttttttttt	sC   !C   
E>
'E92AD;5E>;
E6"E1'E91E66E99E>dangerous commandsession_keyr  Optional[dict]c                B  K   | j         rt          st          dd          S 	 |}|r|                    d          r|d         }| j                             t          |                    }|s-| j                             t          |                     d{V }d}t          |          |k    r|n|d|dz
           dz   }	t          j	        d	d
|	 dt          j
                                                  }
|
                    d|d           t          || j        | j                  }|                    |
|           d{V }t          dt#          |j                            S # t&          $ r(}t          dt#          |                    cY d}~S d}~ww xY w)u   
        Send a button-based exec approval prompt for a dangerous command.

        The buttons call ``resolve_gateway_approval()`` to unblock the waiting
        agent thread — this replaces the text-based ``/approve`` flow on Discord.
        Fr'  r(  r*  N  r^  r_  u    ⚠️ Command Approval Requiredz```
z
```r  r  colorReason)r>   r  inliner  rY   allowed_role_idsembedviewTr`  )r!  r:   r   r   r5  r   r6  r   rS   EmbedColororange	add_fieldExecApprovalViewr]   r%  r>  r(   r   r~   )rl   r!  r  r  r  r$  	target_idr   max_desccmd_displayr  r  r   r   s                 r5   send_exec_approvalz!DiscordAdapter.send_exec_approval*  s      | 	D#4 	De?CCCC	;I 2HLL55 2$[1	l..s9~~>>G K $ : :3y>> J JJJJJJJ H%(\\X%=%=''7>XXY\>CZ]bCbKM86K666m**,,  E
 OOUOKKK#'!%!7!%!7  D  5t<<<<<<<<Cds36{{CCCC 	; 	; 	;e3q66:::::::::	;s   EE, ,
F6FFFr  
confirm_idc                  K   | j         rt          st          dd          S 	 |}|r|                    d          r|d         }| j                             t          |                    }|s-| j                             t          |                     d{V }d}	t          |          |	k    r|n|d|	dz
           dz   }
t          j	        |pd	|
t          j
                                        
          }t          ||| j        | j                  }|                    ||           d{V }t          dt!          |j                            S # t$          $ r(}t          dt!          |                    cY d}~S d}~ww xY w)z6Send a three-button slash-command confirmation prompt.Fr'  r(  r*  Nr  r^  r_  Confirmr  )r  r  rY   r  r  Tr`  )r!  r:   r   r   r5  r   r6  r   rS   r  r  r  SlashConfirmViewr]   r%  r>  r(   r   r~   )rl   r!  r  r\  r  r  r$  r  r   r  bodyr  r  r   r   s                  r5   send_slash_confirmz!DiscordAdapter.send_slash_confirmX  s     
 | 	D#4 	De?CCCC	;I 2HLL55 2$[1	l..s9~~>>G K $ : :3y>> J JJJJJJJ H!'llh6677GNhQRlN<SV[<[DM(y m**,,  E $'%!%!7!%!7	  D  5t<<<<<<<<Cds36{{CCCC 	; 	; 	;e3q66:::::::::	;s   D/E 
FF :F Fr  r?   c                  K   | j         rt          st          dd          S 	 |r*|                    d          r|                    d          n|}| j                             t          |                    }|s-| j                             t          |                     d{V }|rd| dnd}t          j        d	| | t          j	        
                                
          }	t          || j        | j                  }
|                    |	|
           d{V }t          dt          |j                            S # t"          $ r(}t          dt          |                    cY d}~S d}~ww xY w)zSend an interactive button-based update prompt (Yes / No).

        Used by the gateway ``/update`` watcher when ``hermes update --gateway``
        needs user input (stash restore, config migration).
        Fr'  r(  r*  Nz (default: r  rA   u   ⚕ Update Needs Your Inputr  r  r  Tr`  )r!  r:   r   r   r5  r   r6  rS   r  r  goldUpdatePromptViewr]   r%  r>  r(   r   r~   )rl   r!  r  r?   r  r$  r  r   default_hintr  r  r   r   s                r5   send_update_promptz!DiscordAdapter.send_update_prompt~  s      | 	D#4 	De?CCCC	;5=h(,,{B[B[h[111ahIl..s9~~>>G K $ : :3y>> J JJJJJJJ7>F33333BLM3%5|55m((**  E
 $'!%!7!%!7  D
  5t<<<<<<<<Cds36{{CCCC 	; 	; 	;e3q66:::::::::	;s   D E 
E6E1+E61E6	providersr   current_modelcurrent_providerc           	     b  K   | j         rt          st          dd          S 	 |}|r|                    d          r|d         }| j                             t          |                    }	|	s-| j                             t          |                     d{V }		 ddlm}
  |
|          }n# t          $ r |}Y nw xY wt          j        dd	|pd
 d| dt          j                                                  }t          |||||| j        | j                  }|	                    ||           d{V }t          dt%          |j                            S # t          $ rI}t(                              d| j        |           t          dt%          |                    cY d}~S d}~ww xY w)u   Send an interactive select-menu model picker.

        Two-step drill-down: provider dropdown → model dropdown.
        Uses Discord embeds + Select menus via ``ModelPickerView``.
        Fr'  r(  r*  Nr   	get_label   ⚙ Model ConfigurationCurrent model: `r   `
Provider: 

Select a provider:r  )r  r  r  r  on_model_selectedrY   r  r  Tr`  z![%s] send_model_picker failed: %s)r!  r:   r   r   r5  r   r6  hermes_cli.providersr  r~   rS   r  r  blueModelPickerViewr]   r%  r>  r(   r   rx   r   r>   )rl   r!  r  r  r  r  r  r$  r  r   r  provider_labelr  r  r   r   s                   r5   send_model_pickerz DiscordAdapter.send_model_picker  s'      | 	D#4 	De?CCCC)	;I 2HLL55 2$[1	l..s9~~>>G K $ : :3y>> J JJJJJJJ2::::::!*+;!<!< 2 2 2!12 M/*}'A	 * *!/* * * m((**  E ##+!1'"3!%!7!%!7  D  5t<<<<<<<<Cds36{{CCCC 	; 	; 	;NN>	1MMMe3q66:::::::::	;sC   A7E B- ,E -B<9E ;B<<BE 
F.%>F)#F.)F.c                    t          |dd          }|%t          |dd          t          |j                  S t          |dd          }|t          |          S dS )zKReturn the parent channel ID for a Discord thread-like channel, if present.r  Nr   r;  )r:  r(   r   )rl   r   r  r;  s       r5   r6  z%DiscordAdapter._get_parent_channel_id  s`    (D11'&$"="="Ivy>>!G[$77	 y>>!tr7   c                    |dS t          t          dd          }|rt          ||          rdS t          |dd          }|t          |d|          }|dk    rdS dS )zCBest-effort check for whether a Discord channel is a forum channel.NFForumChannelTrm  r  r   )r:  rS   r   )rl   r   	forum_clschannel_type
type_values        r5   r7  zDiscordAdapter._is_forum_parent  sx    ?5G^T::	 	GY77 	4w55# wEEJRtur7   rf  c                    t          |dd          }|s;|r9t          |dd          }|r&|                     |          rt          |dd          }|S )zUReturn the channel topic, falling back to the parent forum's topic for forum threads.topicNr  )r:  r7  )rl   r   rf  r  r  s        r5   rl  z#DiscordAdapter._get_effective_topic  sf    $// 	7 	7Wh55F 7$//77 766r7   rJ  c                   t          |dd          pt          t          |dd                    }t          |dd          }t          |dd          pt          |dd          }t          |dd          }t          |dd          }|                     |          r|r|r
| d| d| S |r|r
| d| d| S |r| d| S |S )	zdBuild a readable chat name for thread-like Discord channels, including forum context when available.r>   Nr   rJ  r  ry  ry  rd  )r:  r(   r7  )rl   rJ  rO  r  ry  rz  parent_names          r5   _format_thread_chat_namez'DiscordAdapter._format_thread_chat_name  s   ffd33[s764QY;Z;Z7[7[400..P'&'42P2PUFD11
ffd33  (( 	CZ 	CK 	C BB[BB[BBB 	D: 	D CCkCCkCCC 	4!33k333r7   Optional[bytes]c           	       K   t          |dd          }|t          |          sdS 	  |             d{V S # t          $ rG}t                              dt          |dd          pt          |dd          |           Y d}~dS d}~ww xY w)a5  Read an attachment via discord.py's authenticated bot session.

        Returns the raw bytes on success, or ``None`` if ``att`` doesn't
        expose a callable ``read()`` or the read itself fails. Callers
        should treat ``None`` as a signal to fall back to the URL-based
        downloaders.
        r  Nz9[Discord] Authenticated attachment read failed for %s: %srW  urlz	<unknown>)r:  callabler~   rx   r   )rl   attreaderr   s       r5   _read_attachment_bytesz%DiscordAdapter._read_attachment_bytes'  s       fd++>&!1!1>4	>>>>>>! 	 	 	NNKZ..R'#uk2R2R  
 44444	s   8 
B	<BB	r  c                   K   |                      |           d{V }|D	 t          ||          S # t          $ r%}t                              d|           Y d}~nd}~ww xY wt          |j        |           d{V S )zCache a Discord image attachment to local disk.

        Primary path: ``att.read()`` + ``cache_image_from_bytes``
        (authenticated, no SSRF gate).

        Fallback: ``cache_image_from_url`` (plain httpx, SSRF-gated).
        Nr  zR[Discord] cache_image_from_bytes rejected att.read() data; falling back to URL: %s)r  r!   r~   rx   r   r    r  rl   r  r  	raw_bytesr   s        r5   _cache_discord_imagez#DiscordAdapter._cache_discord_image<  s       55c::::::::	 -iSAAAA   h       
 *#'s;;;;;;;;;;   2 
A!AA!c                   K   |                      |           d{V }|D	 t          ||          S # t          $ r%}t                              d|           Y d}~nd}~ww xY wt          |j        |           d{V S )zCache a Discord audio attachment to local disk.

        Primary path: ``att.read()`` + ``cache_audio_from_bytes``
        (authenticated, no SSRF gate).

        Fallback: ``cache_audio_from_url`` (plain httpx, SSRF-gated).
        Nr  z@[Discord] cache_audio_from_bytes failed; falling back to URL: %s)r  r#   r~   rx   r   r"   r  r  s        r5   _cache_discord_audioz#DiscordAdapter._cache_discord_audioO  s       55c::::::::	 -iSAAAA   V       
 *#'s;;;;;;;;;;r  c                  K   |                      |           d{V }||S t          |j                  st          d|j                   ddl}ddlm}m}  |d          } ||          \  }}	 |j        di |4 d{V }
 |
j	        |j        fd|
                    d	          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 )a  Download a Discord document attachment and return the raw bytes.

        Primary path: ``att.read()`` (authenticated, no SSRF gate).

        Fallback: SSRF-gated ``aiohttp`` download. This closes the gap
        where the old document path made raw ``aiohttp.ClientSession``
        requests with no safety check (#11345). The caller is responsible
        for passing the returned bytes to ``cache_document_from_bytes``
        (and, where applicable, for injecting text content).
        Nz1Blocked unsafe attachment URL (SSRF protection): r   rx  rM  rN  r  r  rz  r{  zHTTP r;   )r  r&   r  r"  r  r  rK  ry  r  r   r  r  r~   r  )rl   r  r  r  r  rK  ry  r  r  r  ra  r  s               r5   _cache_discord_documentz&DiscordAdapter._cache_discord_documentb  s      55c::::::::	  37## 	MCGMM   	VVVVVVVV""ODDD44V<<'(7(44844 	) 	) 	) 	) 	) 	) 	)"w{ --B-77   ) ) ) ) ) ) ) ) ;#%%#$9DK$9$9:::!YY[[(((((() ) ) ) ) ) ) ) ) ) ) ) )	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)) ) ) ) ) ) ) ) ) ) ) ) ) ) )	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	) 	)s6   0D=7<D3D=
D"	"D=%D"	&D==
E
Er]  c                  K   d}d}t          |j        t          j                  }|r3t	          |j        j                  }|                     |j                  }d}|j                                        }|}d}| j	        j
        r| j	        j
        |j        v rd}|                    d| j	        j
        j         dd                                          }|                    d| j	        j
        j         dd                                          }||_        t          |j        t          j                  st	          |j        j                  h}	|r|	                    |           t          j        dd          }
|
rKd	 |
                    d
          D             }d|vr(|	|z  s#t$                              d| j        |	           dS t          j        dd          }d |                    d
          D             }d|v s|	|z  r#t$                              d| j        |	           dS |                                 }|r|	                    |           |                                 }d | j                                        D             }t	          |j        j                  }||v }d|v pt3          |	|z            p|}|o|| j        v }|r|s|s| j	        j
        |j        vr|sdS d}|s%t          |j        t          j                  st          j        dd          }d |                    d
          D             }t3          |	|z            p|}t          j        dd                                          dv }t9          |dd          t          j        j        k    }|rn|sl|sj|sh|                     |           d{V }|rKt	          |j        j                  }d}t	          |j                  }|}| j                             |           t:          j!        }|"                    d          rt:          j#        }n|j$        r|j$        D ]}|j%        r|j%        "                    d          rt:          j&        }n|j%        "                    d          rt:          j'        }n|j%        "                    d          rt:          j(        }nYd}|j)        r;t          j*        +                    |j)                  \  }}|                                }|tX          v rt:          j-        } n|p|j        }t          |j        t          j                  rd} |j.        j        }!n|rd} | /                    |          }!nfd} t9          |j        dt	          |j        j                            }!ta          |j        d          r"|j        j1        r|j        j1        j         d |! }!| 2                    |j        |!          }"t9          |dd          }#| 3                    t	          |j                  |!| t	          |j.        j                  |j.        j4        ||"t9          |j.        d"d          |#rt	          |#j                  nd|t	          |j                  #          }$g }%g }&d}'|j$        D ]9}|j%        pd$}(|("                    d          r	 d%|(                    d          d&                             d'          d(         z   })|)d)vrd*})| 5                    ||)           d{V }*|%6                    |*           |&6                    |(           to          d+|* d,           # tp          $ rN}+to          d-|+ d,           |%6                    |j9                   |&6                    |(           Y d}+~+d}+~+ww xY w|("                    d          r	 d%|(                    d          d&                             d'          d(         z   })|)d.vrd/})| :                    ||)           d{V }*|%6                    |*           |&6                    |(           to          d0|* d,           # tp          $ rN}+to          d1|+ d,           |%6                    |j9                   |&6                    |(           Y d}+~+d}+~+ww xY wd})|j)        r;t          j*        +                    |j)                  \  }})|)                                })|)s5|(r3d2 tY          j;                    D             },|,<                    |(d          })|)tX          vr t$          =                    d3|)pd$|(           d4}-|j>        r3|j>        |-k    r(t$          =                    d5|j>        |j)                   	 | ?                    ||)           d{V }.t          |.|j)        pd6|)           }*tX          |)         }/|%6                    |*           |&6                    |/           t$          A                    d7|*           d8}0|)d9v rpt          |.          |0k    r]	 |.C                    d:          }1|j)        pd6|) }2t          jE        d;d<|2          }2d=|2 d>|1 }3|'r|' d?|3 }'n|3}'n# t          $ r Y nw xY w # tp          $ r.}+t$          =                    d@|j)        |+dA           Y d}+~+3d}+~+ww xY w|}4|'r|4r|' d?|4 n|'}4|4r|4                                sdB}4|j        }5t	          t9          |5dCd          pd          }6t	          t9          |5dDd                    }7| G                    |7|6pd          }8| H                    |7|6pd          }9d}:d};|jI        rBt	          |jI        jJ                  }:|jI        jK        rt9          |jI        jK        dEd          pd};t          |4||$|t	          |j                  |%|&|:|;|jM        |8|9F          }<|r| j                             |           |t:          j!        k    r"| jN        d(k    r| O                    |<           dS | P                    |<           d{V  dS )Gz!Handle incoming Discord messages.NFTr+   r,   rA   r-   r.  c                ^    h | ]*}|                                 |                                 +S r;   r0  r   r  s     r5   rC  z1DiscordAdapter._handle_message.<locals>.<setcomp>  s2    #g#g#g2\^\d\d\f\f#gBHHJJ#g#g#gr7   rD  r2  z0[%s] Ignoring message in non-allowed channel: %sr3  c                ^    h | ]*}|                                 |                                 +S r;   r0  r   s     r5   rC  z1DiscordAdapter._handle_message.<locals>.<setcomp>  s2    cccrXZX`X`XbXbc

cccr7   z,[%s] Ignoring message in ignored channel: %sc                ,    h | ]}t          |          S r;   r  )r   ch_ids     r5   rC  z1DiscordAdapter._handle_message.<locals>.<setcomp>  s    [[[uE

[[[r7   DISCORD_NO_THREAD_CHANNELSc                ^    h | ]*}|                                 |                                 +S r;   r0  r   s     r5   rC  z1DiscordAdapter._handle_message.<locals>.<setcomp>  s2    !g!g!g\^\d\d\f\f!g"((**!g!g!gr7   DISCORD_AUTO_THREADrC   rf  rm  r  zimage/zvideo/zaudio/rw  rJ  rc  r>   ry  rd  re  r`  )r!  rh  r}  r   rL  r*  ri  r  r  parent_chat_idr3  r   r  r   ;r   ).jpgz.jpegz.pngz.gifz.webpr	  z[Discord] Cached user image: )flushz,[Discord] Failed to cache image attachment: ).oggz.mp3r  z.webmz.m4ar  z[Discord] Cached user audio: z,[Discord] Failed to cache audio attachment: c                    i | ]\  }}||	S r;   r;   )r   rV  r  s      r5   r  z2DiscordAdapter._handle_message.<locals>.<dictcomp>J  s    "U"U"UDAq1a"U"U"Ur7   z7[Discord] Unsupported document type '%s' (%s), skippingi   z5[Discord] Document too large (%s bytes), skipping: %sdocumentz"[Discord] Cached user document: %si  )z.mdz.txtz.logzutf-8z	[^\w.\- ]r   z[Content of z]:
z

z)[Discord] Failed to cache document %s: %sr  z.(The user sent a message with no text content)r;  r   r"  )ra  rj  r  r  r3  
media_urlsmedia_typesreply_to_message_idreply_to_textr   r|  rk  )Qr   r   rS   r5  r(   r   r6  r"  r/   r!  r   rc  r  rp  r  rG   rH   r  rx   r   r>   r  r  r.  r  r8   r5  r4   r:  r   rn  r  rw  ro  r0   rn  r  r  PHOTOVIDEOAUDIOrW  r  splitextr%   DOCUMENTrl  r  r   ry  rl  rm  rz  r  r   r  r~   r  r  r  r   r   sizer  r$   ry   r   r   r  r  UnicodeDecodeErrorr~  rp  r0  r3  resolvedr   
created_atr*  _enqueue_text_eventr  )=rl   r\  r*  parent_channel_idrf  is_voice_linked_channelraw_contentnormalized_contentmention_prefixr:  allowed_channels_rawallowed_channelsignored_channels_rawignored_channelsfree_channelsr  voice_linked_idscurrent_channel_idis_free_channelin_bot_threadauto_threaded_channelno_thread_channels_rawno_thread_channelsskip_threadauto_threadis_reply_messagerJ  rr  r  doc_extr   effective_channelr}  rh  ri  ry  r  r  r  pending_text_injectionr  r  cached_pathr   mime_to_extMAX_DOC_BYTESr  doc_mimeMAX_TEXT_INJECT_BYTEStext_contentrz  	injection
event_textr  r  r  r  r  reply_to_idr  r  s=                                                                r5   rr  zDiscordAdapter._handle_message  s9      	 w??	 	MGO.//I $ ; ;GO L L"' o++--(< 	1!2g6F!F!F!N!3!;!;<XARAU<X<X<XZ\!]!]!c!c!e!e!3!;!;<Y$,BSBV<Y<Y<Y[]!^!^!d!d!f!f0GO'/7+<== *	w1223K  3 1222 $&9-G#L#L # #g#g9M9S9STW9X9X#g#g#g ...FV8V.LL!SUYU^`klllF $&9-G#L#L cc5I5O5OPS5T5Tccc&&&;9I+I&KTYXcddd @@BBM  3 1222";;==O  \[8Q8X8X8Z8Z[[[!$W_%7!8!8&8<L&L#}$ +m344+*  &D)t}*DM  } <$G,<<<^<F
 !% 	2GOW=N!O!O 	2%'Y/KR%P%P"!g!g7M7S7STW7X7X!g!g!g{-??@@SOK)$96BBHHJJNbbK&w==ATAZZ 2; 27N 2Wg 2#77@@@@@@@@ 2(+GO,>(?(?% $I #FII,2)M&&y111 #((-- 	"*HH  	*  # '228<< <#.#4)44X>> 
<#.#4)44X>> <#.#4"$< 6)+)9)9#,)G)GJAw&-mmooG"&>>>'2';HE" 2DW_ gow'899 
	KI+II 	K I556GHHIIIW_=O9P9PQQIw00 KW_5J K&49JJyJJ	
 ..w).TT
 $//""),--)**n1!7>5%88&+5S]]],7: # 
 
  
04& O	 O	C+8yL&&x00 M5 2 23 7 7 ; A A# F Fq IIC"LLL$(,(A(A#s(K(K"K"K"K"K"K"KK%%k222&&|444G+GGtTTTTT  5 5 5LLLTXYYYY%%cg...&&|44444444	5
 ((22 >5 2 23 7 7 ; A A# F Fq IIC"KKK$(,(A(A#s(K(K"K"K"K"K"K"KK%%k222&&|444G+GGtTTTTT  5 5 5LLLTXYYYY%%cg...&&|444444445 < &W--cl;;FAs))++C <| <"U"U4L4R4T4T"U"U"UK%//,;;C666NNQ(y,   
 %5Mx !CH}$<$<SHcl   
.2.J.J3PS.T.T(T(T(T(T(T(TI*C )3<+K;Kc;K;K+ +K (@'DH&--k:::'..x888"KK(LkZZZ4>1"&===#i..TiBiBi
!)3<3C3CG3L3LL36<3SCScCSCSL356,\3Z3ZL0_|0_0_Q]0_0_I'= %KDZAkAk`iAkAk(>(>AJ(>'9 !) !) !)$(D!)(   "NN K #a$ +         (
! 	oHRn2DD
DDDXnJ  	J!1!1!3!3 	JIJR88>B??
wudB//00..x9KtLL66xAStTT 	]g/:;;K ) ] '(9(BIt T T \X\!7:!# +'(*
 
 
"  	*My))) {'''D,JQ,N,N$$U+++++%%e,,,,,,,,,,,sr   B]$$
^<.A^77^<Ba..
c8Acc*B!j*Ajj*
j%"j*$j%%j**
k"4#kk"c                    ddl m}  ||j        | j        j                            dd          | j        j                            dd                    S )z-Session-scoped key for text message batching.r   )build_session_keygroup_sessions_per_userTthread_sessions_per_userF)r>  r?  )gateway.sessionr=  r  r  r<  r   )rl   r  r=  s      r5   _text_batch_keyzDiscordAdapter._text_batch_key  sg    555555  L$(K$5$9$9:SUY$Z$Z%)[%6%:%:;UW\%]%]
 
 
 	
r7   c                   |                      |          }| j                            |          }t          |j        pd          }|||_        || j        |<   nw|j        r$|j        r|j         d|j         n|j        |_        ||_        |j        r>|j                            |j                   |j                            |j                   | j	                            |          }|r(|
                                s|                                 t          j        |                     |                    | j	        |<   dS )zBuffer a text event and reset the flush timer.

        When Discord splits a long user message at 2000 chars, the chunks
        arrive within a few hundred milliseconds.  This merges them into
        a single event before dispatching.
        rA   Nr  )rA  r,  r   r   ra  _last_chunk_lenr  r   r  r-  rV  rW  r"  rX  _flush_text_batch)rl   r  r  r  	chunk_len
prior_tasks         r5   r  z"DiscordAdapter._enqueue_text_event  sL    ""5))-11#66
(b))	$-E!.3D&s++z bDLM a8= @ @EJ @ @ @W\Wa'0H$ ?#**5+;<<<$++E,=>>>377<<
 	 joo// 	 .5.A""3''/
 /
&s+++r7   r  c                |  K   t          j                    }	 | j                            |          }|rt	          |dd          nd}|| j        k    r| j        }n| j        }t          j        |           d{V  | j        	                    |d          }|s<	 | j
                            |          |u r| j
        	                    |d           dS dS t                              d|t          |j        pd                     t          j        |                     |                     d{V  n# t           j        $ r Y nw xY w| j
                            |          |u r| j
        	                    |d           dS dS # | j
                            |          |u r| j
        	                    |d           w w xY w)zWait for the quiet period then dispatch the aggregated text.

        Uses a longer delay when the latest chunk is near Discord's 2000-char
        split point, since a continuation chunk is almost certain.
        rC  r   Nz+[Discord] Flushing text batch %s (%d chars)rA   )r"  current_taskr,  r   r:  _SPLIT_THRESHOLDr+  r*  r  r   r-  rx   ry   r   ra  shieldr  r  )rl   r  rH  pendinglast_lendelayr  s          r5   rD  z DiscordAdapter._flush_text_batch  s      +--	>044S99GAHOww(91===aH4000<6-&&&&&&&&&.223==E ( -11#66,FF.223===== GF' KK=S)r**   .!4!4U!;!;<<<<<<<<<<% 	 	 	 D		 -11#66,FF.223===== GFt-11#66,FF.223==== Gs1   B D1 AD1 0F 1E F EF :F;)r  r   r)   r8   rq  )r)   r(   )r  r  r)   r  )r  r	   r)   r  )r  r	   r)   r  )r)   r  )r\  r	   r
  r(   r)   r8   )r  r   r)   r  )r  r   r  r   r)   r  )NN)
r!  r(   r"  r(   r#  r  r$  r%  r)   r   )rE  r	   r"  r(   r)   r   )rE  r	   rO  r  r"  r(   rS  r	   rT  rU  r)   r   )
r!  r(   r3  r(   r"  r(   r\  r8   r)   r   )
r!  r(   rc  r(   rd  r  re  r  r)   r   )Nro  )
r!  r(   rp  rq  r$  r%  rr  r)  r)   r  )r!  r(   r  r(   r)   r   )NNN)r!  r(   r  r(   rd  r  r#  r  r$  r%  r)   r   )r  r   r)   r  )r  r   r  r(   r)   r8   )r  r   r   r(   )r  r   r)   r8   )r  r   r)   r%  )r  r   r)   r(   )r  r   )r  r   r   r   r   rq   r   )r   r(   r)   r8   )r*  r+  r)   r,  )r*  r+  r@  r(   r)   r8   )r*  r+  r@  r(   rC  r(   r)   r8   )
rL  r(   r   r(   r@  r(   rC  r(   r)   r  )r!  r(   rX  r(   rd  r  r#  r  r$  r%  r)   r   )r!  r(   r  r(   rd  r  r#  r  r$  r%  r)   r   )r!  r(   rc  r(   rd  r  r#  r  r$  r%  r)   r   )r!  r(   rg  r(   rd  r  r#  r  r$  r%  r)   r   )NNNN)r!  r(   rc  r(   rd  r  re  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  ra  r(   r)   r   r&  )
r*  r  r>   r(   r\  r(   r  r   r)   r  )
r*  r  r*  r(   rO  r(   ra  r(   r)   r  )r  r(   r;  r  r)   r  )r  r(   r;  r  r)   r  )r)   rZ   )r   r	   r)   r	   )r*  r  r)   r  )
r*  r  r>   r(   r\  r(   r  r   r)   r  )r\  r  r)   r  )r  N)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   )rA   rA   N)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)   r8   )F)r   r	   rf  r8   r)   r  )rJ  r	   r)   r(   )r)   r  )r  r(   r)   r(   )r  r(   r)   rq   )r\  r]  r)   r  )r  r   r)   r(   )r  r(   r)   r  )^r  r  r  r  r;  rI  r  rn   r  r  rY  r  r  r  r  r  r  r  r  r  r  r  r  r   r>  r8  r[  rb  rn  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r	  ro  r?  rE  rD  rK  r[  r_  re  ri  rk  rr  rt  r~  rU  r9  r  r  r,  r+  rJ  r`  r  r  rx  r~  rp  r  r  r}  r  ru  r  r  r  r  r  r6  r7  rl  r  r  r  r  r  rr  rA  r  rD  __classcell__r>  s   @r5   r  r    s(	          M&S &S &S &S &S &SPz z z zx4 4 4 4>*^ *^ *^ *^X
 
 
 

 
 
 
0    \   .
 
 
 
6
 
 
 
H
 H
 H
 H
T	 	 	 		 	 	 	Z Z Z Z6 6 6 6
9 
9 
9 
9  #'-1h; h; h; h; h;T5
 5
 5
 5
v &* $9
 9
 9
 9
 9
 9
B ; ; ; ; ; ;: "&#'@ @ @ @ @J .2 ~ ~ ~ ~ ~ ~ ~@W W W W, "&"&-1Ug Ug Ug Ug Ug Ug Ugv       D4 4 4 4* )" )" )" )"V
$ 
$ 
$ 
$
 
 
 
   ,4 4 4 4
1
 1
 1
 1
f       . J J J J@! ! ! !F) ) ) ) )vU U U Un
 
 
 
"/ / / /b0T 0T 0T 0Tl "&"&-1l l l l l l l* "&"&-1MS MS MS MS MS MS MSf "&"&-1Cn Cn Cn Cn Cn Cn CnR "&"&-1g g g g g g g* "&#'"&-1t t t t t t t$ J  J  J  J  JD   %I %I %I %IN>g >g >g >g@    $(	-F -F -F -F -F^H4 H4 H4 H4T#
 #
 #
 #
JGY GY GY GYR0 0 0 04J J J J@+
 +
 +
 +
j %)%^ %^ %^ %^ %^N() () () ()T
P 
P 
P 
P 
PP P P P P
g g g g   .; ; ; ;   . %)E E E E E EV' ' ' 'V /#',; ,; ,; ,; ,;` 59$; $; $; $; $;N 9;-1 ;  ;  ;  ;  ;T .2;; ;; ;; ;; ;;z             R   *< < < <&< < < <& )  )  )  )Dc- c- c- c-R	
 
 
 

 
 
 
8&> &> &> &> &> &> &> &>r7   r  rY   Optional[set]r  c                   |pt                      }|pt                      }t          |          }t          |          }|s|sdS t          | dd          }|dS |r0	 t          |j                  }n# t
          $ r d}Y nw xY w|r||v rdS |r;t          |dd          }	|	dS 	 d |	D             }
n# t          $ r Y dS w xY w|
|z  rdS dS )a?  Shared user-or-role OR semantics for component view button clicks.

    Mirrors ``DiscordAdapter._is_allowed_user`` / the slash and on_message
    gates so every Discord interaction surface honors the same trust
    boundary. Component views (ExecApprovalView, SlashConfirmView,
    UpdatePromptView, ModelPickerView) used to receive only
    ``allowed_user_ids``: in role-only deployments
    (DISCORD_ALLOWED_ROLES set, DISCORD_ALLOWED_USERS empty) the user
    set was empty and the legacy "no allowlist = allow everyone" branch
    let any guild member click the buttons -- approving exec commands,
    cancelling slash confirmations, switching the model.

    Behavior:

      - both allowlists empty -> allow (preserves existing no-allowlist
        deployments, no regression)
      - user is in user allowlist -> allow
      - role allowlist set + user has a role in it -> allow
      - role allowlist set + interaction.user has no resolvable
        ``roles`` attribute (e.g. DM context with a role policy active)
        -> reject (fail closed)
      - otherwise -> reject
    Tr   NFrA   rP   c                0    h | ]}t          |d d          S r  r  )r   r  s     r5   rC  z(_component_check_auth.<locals>.<setcomp>8  s$    HHHWQd33HHHr7   )rZ   r8   r:  r(   r   AttributeErrorr!  )r*  rY   r  user_setrole_setr%  r&  r   r   
roles_attruser_role_idss              r5   _component_check_authrY     s3   8  (355H(355HXIXI Y t;--D|u 	dg,,CC 	 	 	CCC	 	3(??4 T7D11

 5	HHZHHHMM 	 	 	55	8# 	45s$   A2 2B B$B1 1
B?>B?c                      e Zd ZdZ	 d d! fd	Zd"dZd#dZej        	                    dej
        j                  d$d            Zej        	                    dej
        j                  d$d            Zej        	                    dej
        j                  d$d            Zej        	                    dej
        j                  d$d            Zd Z xZS )%r  u  
        Interactive button view for exec approval of dangerous commands.

        Shows four buttons: Allow Once, Allow Session, Always Allow, Deny.
        Clicking a button calls ``resolve_gateway_approval()`` to unblock the
        waiting agent thread — the same mechanism as the text ``/approve`` flow.
        Only users in the allowed list can click.  Times out after 5 minutes.
        Nr  r(   rY   rZ   r  rQ  c                    t                                          d           || _        || _        |pt	                      | _        d| _        d S Nr  r_  Fr  rn   r  rY   rZ   r  r  rl   r  rY   r  r>  s       r5   rn   zExecApprovalView.__init__M  L     GGS)))*D$4D!$4$=D!!DMMMr7   r*  r  r)   r8   c                8    t          || j        | j                  S )z'Verify the user clicking is authorized.rY  rY   r  rl   r*  s     r5   _check_authzExecApprovalView._check_authY  s!    (T2D4I  r7   r  r  discord.ColorrA  c                  K   | j         r$|j                            dd           d{V  dS |                     |          s$|j                            dd           d{V  dS d| _         |j        j        r|j        j        d         nd}|r,||_        |                    | d|j        j	                    | j
        D ]	}d|_        
|j                            || 	           d{V  	 dd
lm}  || j        |          }t                               d|| j        ||j        j	                   dS # t$          $ r&}	t                               d|	           Y d}	~	dS d}	~	ww xY w)zIResolve the approval via the gateway approval queue and update the embed.z(This approval has already been resolved~TrG  Nz*You're not authorized to approve commands~r    by ra  r  )resolve_gateway_approvalzJDiscord button resolved %d approval(s) for session %s (choice=%s, user=%s)z2Failed to resolve gateway approval from button: %s)r  rI  rJ  rc  r\  embedsr  
set_footerr   rz  childrendisabledrb  tools.approvalrh  r  rx   ry   r~   r  )
rl   r*  r  r  rA  r  childrh  countrM  s
             r5   _resolvezExecApprovalView._resolve_  s     
 } !*77>$ 8          ##K00 !*77@D 8           DM 6A5H5OYK'.q11UYE U#  &S&SK4D4Q&S&S TTT  & &!%&33%d3KKKKKKKKKXCCCCCC001A6JJ`4+V[5E5R      X X XQSVWWWWWWWWWXs   0AD6 6
E& E!!E&z
Allow OncerA  stylebuttondiscord.ui.Buttonc                ~   K   |                      |dt          j                                        d           d {V  d S NoncezApproved oncerp  rS   r  greenrl   r*  rs  s      r5   
allow_oncezExecApprovalView.allow_once  F       --VW]5H5H5J5JO\\\\\\\\\\\r7   zAllow Sessionc                ~   K   |                      |dt          j                                        d           d {V  d S )Nra  zApproved for session)rp  rS   r  r  rz  s      r5   allow_sessionzExecApprovalView.allow_session  sG       --Y8J8J8L8LNdeeeeeeeeeeer7   zAlways Allowc                ~   K   |                      |dt          j                                        d           d {V  d S )NalwayszApproved permanentlyrp  rS   r  purplerz  s      r5   allow_alwayszExecApprovalView.allow_always  sG       --Xw}7K7K7M7MOefffffffffffr7   Denyc                ~   K   |                      |dt          j                                        d           d {V  d S )Nr  Denied)rp  rS   r  redrz  s      r5   r  zExecApprovalView.deny  sF       --VW]5F5F5H5H(SSSSSSSSSSSr7   c                :   K   d| _         | j        D ]	}d|_        
dS )z;Handle view timeout -- disable buttons and mark as expired.TNr  rk  rl  rl   rn  s     r5   
on_timeoutzExecApprovalView.on_timeout  s1       DM & &!%& &r7   r   r  r(   rY   rZ   r  rQ  r*  r  r)   r8   r*  r  r  r(   r  rd  rA  r(   r*  r  rs  rt  )r  r  r  r  rn   rc  rp  rS   uirs  ButtonStylery  r{  greyr~  blurpler  r  r  r  rO  rP  s   @r5   r  r  C  s       	 	 /3	
	" 
	" 
	" 
	" 
	" 
	" 
	"	 	 	 	(	X (	X (	X (	XT 
		W5H5N		O	O	] 	] 	] 
P	O	]
 
		8K8P		Q	Q	f 	f 	f 
R	Q	f
 
		w7J7R		S	S	g 	g 	g 
T	S	g
 
		w/B/F		G	G	T 	T 	T 
H	G	T
	& 	& 	& 	& 	& 	& 	&r7   r  c                  ^    e Zd ZdZ	 dd  fd
Zd!dZd"dZej        	                    dej
        j                  d#d            Zej        	                    dej
        j                  d#d            Zej        	                    dej
        j                  d#d            Zd Z xZS )$r  u  Three-button view for generic slash-command confirmations.

        Used by ``/reload-mcp`` and any future slash command routed through
        ``GatewayRunner._request_slash_confirm``.  Buttons map to the
        gateway's three choices:

          * "Approve Once"   → ``choice="once"``
          * "Always Approve" → ``choice="always"``
          * "Cancel"         → ``choice="cancel"``

        Clicking calls the module-level
        ``tools.slash_confirm.resolve(session_key, confirm_id, choice)``
        which runs the handler the runner stored for this ``session_key``.
        Only users in the adapter's allowlist can click.  Times out after
        5 minutes (matches the gateway primitive's timeout).
        Nr  r(   r  rY   rZ   r  rQ  c                    t                                          d           || _        || _        || _        |pt                      | _        d| _        d S r\  )r  rn   r  r  rY   rZ   r  r  )rl   r  r  rY   r  r>  s        r5   rn   zSlashConfirmView.__init__  sS     GGS)))*D(DO$4D!$4$=D!!DMMMr7   r*  r  r)   r8   c                8    t          || j        | j                  S r   ra  rb  s     r5   rc  zSlashConfirmView._check_auth  !    (T2D4I  r7   r  r  rd  rA  c                D  K   | j         r$|j                            dd           d {V  d S |                     |          s$|j                            dd           d {V  d S d| _         |j        j        r|j        j        d         nd }|r,||_        |                    | d|j        j	                    | j
        D ]	}d|_        
|j                            ||            d {V  	 dd	lm} |                    | j        | j        |           d {V }|r |j                            |           d {V  t(                              d
| j        ||j        j	                   d S # t,          $ r(}	t(                              d|	d           Y d }	~	d S d }	~	ww xY w)Nz&This prompt has already been resolved~TrG  z,You're not authorized to answer this prompt~r   rf  rg  r  )slash_confirmzIDiscord button resolved slash-confirm for session %s (choice=%s, user=%s)z(Discord slash-confirm resolve failed: %sr  )r  rI  rJ  rc  r\  ri  r  rj  r   rz  rk  rl  rb  toolsr  resolver  r  rv  r>  rx   ry   r~   r  )
rl   r*  r  r  rA  r  rn  _slash_confirm_modresult_textrM  s
             r5   rp  zSlashConfirmView._resolve  sx      } !*77< 8          ##K00 !*77Bd 8           DM5@5H5OYK'.q11UYE U#  &S&SK4D4Q&S&S TTT & &!%&33%d3KKKKKKKKK]EEEEEE$6$>$>$dov% %        A%.33K@@@@@@@@@+$fk.>.K    
  ] ] ]GW[\\\\\\\\\]s   0A;E- -
F7FFzApprove Oncerq  rs  rt  c                ~   K   |                      |dt          j                                        d           d {V  d S rv  rx  rz  s      r5   approve_oncezSlashConfirmView.approve_once  r|  r7   zAlways Approvec                ~   K   |                      |dt          j                                        d           d {V  d S )Nr  zAlways approvedr  rz  s      r5   approve_alwayszSlashConfirmView.approve_always  sG       --Xw}7K7K7M7MO`aaaaaaaaaaar7   Cancelc                ~   K   |                      |dt          j                                        d           d {V  d S )NrW  	Cancelled)rp  rS   r  greyplerz  s      r5   rW  zSlashConfirmView.cancel  sG       --Xw}7L7L7N7NP[\\\\\\\\\\\r7   c                :   K   d| _         | j        D ]	}d|_        
d S r   r  r  s     r5   r  zSlashConfirmView.on_timeout
  1       DM & &!%& &r7   r   )r  r(   r  r(   rY   rZ   r  rQ  r  r  r  )r  r  r  r  rn   rc  rp  rS   r  rs  r  ry  r  r  r  r  rW  r  rO  rP  s   @r5   r  r    sQ       	 	, /3	" 	" 	" 	" 	" 	" 	"	 	 	 	
*	] *	] *	] *	]X 
		w7J7P		Q	Q	] 	] 	] 
R	Q	]
 
		!19L9T		U	U	b 	b 	b 
V	U	b
 
		1D1H		I	I	] 	] 	] 
J	I	]
	& 	& 	& 	& 	& 	& 	&r7   r  c                      e Zd ZdZ	 dd fd	Zd dZd!dZej        	                    dej
        j        d          d"d            Zej        	                    dej
        j        d          d"d            Zd Z xZS )#r  aI  Interactive Yes/No buttons for ``hermes update`` prompts.

        Clicking a button writes the answer to ``.update_response`` so the
        detached update process can pick it up.  Only authorized users can
        click.  Times out after 5 minutes (the update process also has a
        5-minute timeout on its side).
        Nr  r(   rY   rZ   r  rQ  c                    t                                          d           || _        || _        |pt	                      | _        d| _        d S r\  r]  r^  s       r5   rn   zUpdatePromptView.__init__  r_  r7   r*  r  r)   r8   c                8    t          || j        | j                  S r   ra  rb  s     r5   rc  zUpdatePromptView._check_auth$  r  r7   answerr  rd  rA  c                >  K   | j         r$|j                            dd           d {V  d S |                     |          s$|j                            dd           d {V  d S d| _         |j        j        r|j        j        d         nd }|r,||_        |                    | d|j        j	                    | j
        D ]	}d|_        
|j                            ||            d {V  	 dd	lm}  |            }|d
z  }	|	                    d          }
|
                    |           |
                    |	           t$                              d||j        j	                   d S # t(          $ r&}t$                              d|           Y d }~d S d }~ww xY w)NzAlready answered~TrG  You're not authorized~r   rf  rg  r  )get_hermes_homez.update_responsez.tmpz)Discord update prompt answered '%s' by %sz#Failed to write update response: %s)r  rI  rJ  rc  r\  ri  r  rj  r   rz  rk  rl  rb  hermes_constantsr  with_suffix
write_textr  rx   ry   r~   r  )rl   r*  r  r  rA  r  rn  r  rW  response_pathtmprM  s               r5   _respondzUpdatePromptView._respond)  sK      } !*77'4 8          ##K00 !*77, 8           DM 6A5H5OYK'.q11UYE U#  &S&SK4D4Q&S&S TTT & &!%&33%d3KKKKKKKKKI<<<<<<&(( $'9 9#//77v&&&M***?K,9      I I IBCHHHHHHHHHIs   0A:E, ,
F6FFYesu   ✓)rA  rr  r
  rs  rt  c                ~   K   |                      |dt          j                                        d           d {V  d S )Nyr  )r  rS   r  ry  rz  s      r5   yes_btnzUpdatePromptView.yes_btnS  sF       --S'-2E2E2G2GOOOOOOOOOOOr7   Nou   ✗c                ~   K   |                      |dt          j                                        d           d {V  d S )NrU  r  )r  rS   r  r  rz  s      r5   no_btnzUpdatePromptView.no_btnY  sF       --S'-2C2C2E2EtLLLLLLLLLLLr7   c                :   K   d| _         | j        D ]	}d|_        
d S r   r  r  s     r5   r  zUpdatePromptView.on_timeout_  r  r7   r   r  r  )r*  r  r  r(   r  rd  rA  r(   r  )r  r  r  r  rn   rc  r  rS   r  rs  r  ry  r  r  r  r  rO  rP  s   @r5   r  r    s       	 	 /3	
	" 
	" 
	" 
	" 
	" 
	" 
	"	 	 	 	
(	I (	I (	I (	IT 
		g.A.Gu		U	U	P 	P 	P 
V	U	P
 
		W-@-DE		R	R	M 	M 	M 
S	R	M
	& 	& 	& 	& 	& 	& 	&r7   r  c                  d     e Zd ZdZ	 dd fdZddZd ZddZddZddZ	ddZ
ddZd Z xZS ) r  u   Interactive select-menu view for model switching.

        Two-step drill-down: provider dropdown → model dropdown.
        Edits the original message in-place as the user navigates.
        Times out after 2 minutes.
        Nr  r   r  r(   r  r  rY   rZ   r  rQ  c                   t                                          d           || _        || _        || _        || _        || _        || _        |pt                      | _	        d| _
        d| _        |                                  d S )Nr   r_  FrA   )r  rn   r  r  r  r  r  rY   rZ   r  r  _selected_provider_build_provider_select)	rl   r  r  r  r  r  rY   r  r>  s	           r5   rn   zModelPickerView.__init__l  s     GGS)))&DN!.D$4D!*D%6D"$4D!$4$=D!!DM+-D#'')))))r7   r*  r  r)   r8   c                8    t          || j        | j                  S r   ra  rb  s     r5   rc  zModelPickerView._check_auth  r  r7   c           
        |                                   g }| j        D ]}|                    dt          |                    dg                               }|d          d| d}|                    d          rdnd}|                    t          j        |dd	         |d
         |                     |sdS t
          j                            d|dd         d          }| j	        |_
        |                     |           t
          j                            dt
          j        j        d          }| j        |_
        |                     |           dS )z!Build the provider dropdown menu.total_modelsmodelsr>   rO  z models)
is_currentr  Nr  slug)rA  r  r  zChoose a provider...r<  model_provider_selectplaceholderr  	custom_idr  model_cancelrA  rr  r  )clear_itemsr  r   r   r   rS   SelectOptionr  Select_on_provider_selectedr  add_itemButtonr  r  
_on_cancel)rl   r  pro  rA  r  select
cancel_btns           r5   r  z&ModelPickerView._build_provider_select  ss   G^ 
 
nc!%%"2E2E.F.FGGV977777$%EE,$7$7AyyT(#DSDki$(       Z&&21 '  F
 #8FOMM&!!! **g&9&= +  J #'/JMM*%%%%%r7   provider_slugc           	     ~   |                                   t          fd| j        D             d          }|sdS |                    dg           }g }|dd         D ]\}d|v r|                    d          d         n|}|                    t          j        |dd         |dd                              ]|sdS t          j        	                    d	|                    d
           d|d          }| j
        |_        |                     |           t          j                            dt          j        j        d          }| j        |_        |                     |           t          j                            dt          j        j        d          }	| j        |	_        |                     |	           dS )z1Build the model dropdown for a specific provider.c              3  4   K   | ]}|d          k    |V  dS r  Nr;   r   r  r  s     r5   rJ  z6ModelPickerView._build_model_select.<locals>.<genexpr>  1      IIqai=.H.H.H.H.H.HIIr7   Nr  r<  r  r   r  )rA  r  zChoose a model from r>   r_  model_model_selectr  u   ◀ Back
model_backr  r  model_cancel2)r  nextr  r   r  r   rS   r  r  r  _on_model_selectedr  r  r  r  r  _on_backr  r  )
rl   r  providerr  r  model_idr   r  back_btnr  s
    `        r5   _build_model_selectz#ModelPickerView._build_model_select  s   IIIIDNIII4 H  \\(B//FG"3B3K  36(??s++B//(#DSDk&ttn       Z&&[8<<3V3V[[[. '  F
 #5FOMM&!!!z(( (;(@L )  H !%HMM(### **g&9&= +  J #'/JMM*%%%%%r7   c           	       K   |                      |          s$|j                            dd           d {V  d S |j        d         d         | _        t          fd| j        D             d           }|r|                    d          n}|                                |r|                    dd          nd}|r1t          t          |                    d	g                     d
          nd}||k    r	d||z
   dnd}|j                            t          j        dd| d| t          j                                                  |            d {V  d S )Nr  TrG  r  r   c              3  4   K   | ]}|d          k    |V  dS r  r;   r  s     r5   rJ  z8ModelPickerView._on_provider_selected.<locals>.<genexpr>  r  r7   r>   r  r  r<  z
*u2    more available — type `/model <name>` directly*rA   r  zProvider: **z**
Select a model:r  r  )rc  rI  rJ  r   r  r  r  r   r  minr   rb  rS   r  r  r  )rl   r*  r  pnamer  shownr<  r  s          @r5   r  z%ModelPickerView._on_provider_selected  s     ##K00 !*77, 8          ',X6q9M&3D#IIIIDNIII4 H <DVHLL777E$$]3337?FHLL333QE@HOCHLL26677<<<aE_dgl_l_l[%%-[[[[rtE&33m3 Pu P P P P!-,,..  
  4           r7   c                  K   | j         r$|j                            dd           d {V  d S |                     |          s$|j                            dd           d {V  d S d| _         |j        d         d         }|                                  |j                            t          j        dd| d	t          j	        
                                
          d            d {V  	 |                     t          |j                  || j                   d {V }n# t          $ r}d| }Y d }~nd }~ww xY w|                    t          j        d|t          j	                                        
          d            d {V  d S )NzAlready resolved~TrG  r  r  r   u   ⚙ Switching ModelzSwitching to `z`...r  r  zError switching model: u   ⚙ Model Switched)r  rI  rJ  rc  r   r  rb  rS   r  r  r  r  r(   r  r  r~   r  ry  )rl   r*  r  r  rM  s        r5   r  z"ModelPickerView._on_model_selected  s^     } !*77'4 8          ##K00 !*77, 8           DM"'1!4H&33m/ ? ? ? ?!-,,..  
  4         >$($:$:.//+% %      
  > > >===> 44m. +!---//  
  5           s   -4D" "
D;,D66D;c           
       K   |                      |          s$|j                            dd           d {V  d S |                                  	 ddlm}  || j                  }n# t          $ r
 | j        }Y nw xY w|j                            t          j
        dd| j        pd d	| d
t          j                                                  |            d {V  d S )Nr  TrG  r   r  r  r  r   r  r  r  r  )rc  rI  rJ  r  r  r  r  r~   rb  rS   r  r  r  r  )rl   r*  r  r  s       r5   r  zModelPickerView._on_back  sn     ##K00 !*77, 8          '')))7::::::!*4+@!A!A 7 7 7!%!67 &33m3.4+=+J . .%3. . . "-,,..    4           s   A( (A<;A<c                   K   d| _         |                                  |j                            t	          j        ddt          j                                                  |            d {V  d S )NTr  zModel selection cancelled.r  r  )r  r  rI  rb  rS   r  r  r  rb  s     r5   r  zModelPickerView._on_cancel5  s       DM&33m3 <!-//11  
  4           r7   c                @   K   d| _         |                                  d S r   )r  r  r   s    r5   r  zModelPickerView.on_timeoutA  s%       DMr7   r   )r  r   r  r(   r  r(   r  r(   rY   rZ   r  rQ  r  )r  r(   r  )r  r  r  r  rn   rc  r  r  r  r  r  r  r  rO  rP  s   @r5   r  r  d  s        	 	 /3	* 	* 	* 	* 	* 	* 	*.	 	 	 	
	& 	& 	&@(	& (	& (	& (	&T	 	 	 	:(	 (	 (	 (	T	 	 	 	8
	 
	 
	 
		 	 	 	 	 	 	r7   r  )r'   r(   r)   r(   rN  )rY   rQ  r  rQ  r)   r8   )K
__future__r   r"  loggingrG   r   r	  r  rc   r   collectionsr   typingr   r   r   r   r	   r
   	getLoggerr  rx   r  r  rS   r   r]  r   discord.extr   r:   r`  r  pathlibr   _Pathr  insertr(   __file__r  parentsgateway.configr   r   r  gateway.platforms.helpersr   r   r  r   r   r   r   r   r    r!   r"   r#   r$   r%   tools.url_safetyr&   r6   r<   rU   rW   r  rY  r  Viewr  r  r  r  r;   r7   r5   <module>r     s,   " " " " " "   				            # # # # # # = = = = = = = = = = = = = = = =		8	$	$$;$;$; !!8!8!8 
NNN::::::::$$$$$$   GNGHHH 


 ! ! ! ! ! ! 33uuX..008;<< = = = 3 3 3 3 3 3 3 3 				 U U U U U U U U                          ) ( ( ( ( (   "   
     F] ] ] ] ] ] ] ]@_8> _8> _8> _8> _8>( _8> _8> _8>Nq> > > >B  Bb& b& b& b& b&7:? b& b& b&Hf& f& f& f& f&7:? f& f& f&PS& S& S& S& S&7:? S& S& S&j_ _ _ _ _'*/ _ _ _ _ _G	B Bs   A, ,A>=A>