
    .cjY                     V   U d 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
 ddlmZ ddlmZ  ej        e          Z e            dz  ZdZi Zeeeef         ef         ed	<    e            d
z  Zdeeeeef         f         fdZdeeef         ddfdZdedefdZdedeeef         defdZdeeef         de
e         fdZdeeef         defdZdededdfdZ deeef         deeef         fdZ!de	eeef                  fdZ"de#de
e         fdZ$de	eeef                  fdZ%dede	eeef                  fd Z&dede	eeef                  fd!Z'dede	eeef                  fd"Z(deeef         fd#Z)ded$ede
e         fd%Z*ded&ede
e         fd'Z+defd(Z,dS ))a6  
Channel directory -- cached map of reachable channels/contacts per platform.

Built on gateway startup, refreshed periodically (every 5 min), and saved to
~/.hermes/channel_directory.json.  The send_message tool reads this file for
action="list" and for resolving human-friendly channel names to numeric IDs.
    N)datetime)AnyDictListOptional)get_hermes_home)atomic_json_writezchannel_directory.jsoni  _slack_directory_warning_lastzchannel_aliases.jsonreturnc                     t                                           si S 	 t          t           d          5 } t          j        |           }d d d            n# 1 swxY w Y   t          |t                    r|ni S # t          $ r i cY S w xY w)Nutf-8encoding)CHANNEL_ALIASES_PATHexistsopenjsonload
isinstancedict	Exception)fdatas     ;/home/ice/.hermes/hermes-agent/gateway/channel_directory.py_load_channel_aliasesr   '   s    &&(( 	&999 	 Q9Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	 !$--5tt25   			s4   A8 AA8 AA8 AA8 8BB	platformsc           	         t                      }|                                D ]+\  }}t          |t                    s|                     |g           }t          |t
                    sH|                                D ]\  }}t          |t                    r|                                s/t          |          }|                                }d}|D ]7}t          |t                    r |                    d          |k    r||d<   d}8|s>|	                    ||t          |          
                    d          rdnddd	           ϐ-dS )
a  Overlay friendly names onto directory entries by chat_id.

    Renames matching entries in place; injects a placeholder entry for an
    aliased id that hasn't been discovered yet (so a freshly-created group is
    addressable by name before its first message). Mutates *platforms*.
    FidnameTz@g.usgroupdmNr   r   type	thread_id)r   itemsr   r   
setdefaultliststrstripgetappendendswith)	r   aliases	plat_nameid_mapentrieschat_idfriendlymatchedes	            r   _apply_channel_aliasesr5   2   sp    $%%G$]]__  	6&$'' 	&&y"55'4(( 	!' 	 	GXh,, HNN4D4D 'llG~~''HG # #a&& #155;;'+A+A (AfI"G !$'*7||'<'<W'E'EOGG4!%	      	     valuec                 t    |                      d                                                                          S )N#)lstripr)   lowerr7   s    r   _normalize_channel_queryr=   S   s,    <<""$$**,,,r6   platform_namechannelc                     |d         }| dk    r|                     d          rd| S | dk    r#|                     d          r| d|d          dS |S )zHReturn the human-facing target label shown to users for a channel entry.r   discordguildr9   r#   z ())r*   )r>   r?   r   s      r   _channel_target_namerD   W   so    6?D	!!gkk'&:&:!4zz	!!gkk&&9&9!,,'&/,,,,Kr6   originc                     |                      d          }|sd S |                      d          }|r| d| S t          |          S )Nr1   r$   :r*   r(   )rE   r1   r$   s      r   _session_entry_idrI   a   sY    jj##G t

;''I (''I'''w<<r6   c                    |                      d          p6|                      d          p!t          |                      d                    }|                      d          }|s|S |                      d          pd| }| d| S )N	chat_name	user_namer1   r$   
chat_topicztopic z / rH   )rE   	base_namer$   topic_labels       r   _session_entry_namerP   k   s    

;''`6::k+B+B`c&**U^J_J_F`F`I

;''I **\**B.By.B.BK))K)))r6   team_iddetailc                 D   t          |           t          |          f}t          j                    }t                              |          }|||z
  t
          k    r(|t          |<   t                              d| |           dS t                              d| |           dS )zKWarn once per team/error per interval for recurring Slack refresh failures.Nz@Channel directory: failed to list Slack channels for team %s: %szQChannel directory: suppressed repeated Slack channel list failure for team %s: %s)	r(   time	monotonicr
   r*   )_SLACK_DIRECTORY_WARNING_INTERVAL_SECONDSloggerwarningdebug)rQ   rR   keynowlasts        r   _warn_slack_directoryr]   u   s    w<<V
%C
.

C(,,S11D|sTz%NNN-0%c*N	
 	
 	
 	
 	
 			
 	
 	
 	
 	
r6   adaptersc                   K   ddl m} i }|                                 D ]\  }}	 ||j        k    r$t	          j        t          |           d{V |d<   n#||j        k    rt          |           d{V |d<   Y# t          $ r+}t                              d|j        |           Y d}~d}~ww xY wt          h d          }d | D             }|D ]9}|j        }	|	|v s|	|v s|	|vrt	          j        t          |	           d{V ||	<   :	 dd	lm}
 |
                                D ]J}|j        |vr?|j        |vr6|j        |v r-t	          j        t          |j                   d{V ||j        <   Kn# t          $ r Y nw xY wt'          |           t)          j                                                    |d
}	 t/          t0          |           n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|S )z
    Build a channel directory from connected platform adapters and session data.

    Returns the directory dict and writes it to DIRECTORY_PATH.
    r   )PlatformNrA   slackz)Channel directory: failed to build %s: %s>   localwebhook
api_serverc           	      J    h | ] }t          |d t          |                    !S r<   )getattrr(   ).0ps     r   	<setcomp>z*build_channel_directory.<locals>.<setcomp>   s*    LLLaga#a&&99LLLr6   )platform_registry
updated_atr   z&Channel directory: failed to write: %s)gateway.configr`   r%   DISCORDasyncio	to_thread_build_discordSLACK_build_slackr   rW   rX   r7   	frozenset_build_from_sessionsgateway.platform_registryrj   plugin_entriesr   r5   r   r[   	isoformatr	   DIRECTORY_PATH)r^   r`   r   platformadapterr4   _SKIP_SESSION_DISCOVERYadapter_platform_namesplatr.   rj   entry	directorys                r   build_channel_directoryr      s      ('''''13I%^^-- [ ['	[8+++-4->~w-W-W'W'W'W'W'W'W	)$$X^+++7+@+@%@%@%@%@%@%@	'" 	[ 	[ 	[NNFXYZZZZZZZZ	[ ((J(J(JKKLL8LLL X XJ	000I%% 666%,%67KY%W%WWWWWWW	)
??????&5577 	b 	bE
"999Ji//J"888.5.?@TV[V`.a.a(a(a(a(a(a(a	%*%	b     9%%% lnn..00 I
D.)4444 D D D?CCCCCCCCD sC   AA88
B-!B((B-
A%E0 0
E=<E=8G 
G=G88G=c           	         g }t          | dd          }|s|S 	 ddl}n# t          $ r |cY S w xY w|j        D ]}|j        D ]8}|                    t          |j                  |j        |j        dd           9t          |dd          pg }|D ]8}|                    t          |j                  |j        |j        dd           9|	                    t          d                     |S )	zGEnumerate all text channels and forum channels the Discord bot can see._clientNr   r?   )r   r   rB   r#   forum_channelsforumrA   )rf   rA   ImportErrorguildstext_channelsr+   r(   r   r   extendru   )r{   channelsclient_discordrB   chforumss          r   rq   rq      sK   HWi..F """""      % 	 	BOO"%jj!	       0$77=2 	 	BOO"%jj	     	 OO(33444Os    --errorc                    t          | dd          }t          |t                    r(|                    d          }|rt	          |          ndS |9	 |                    d          }|rt	          |          ndS # t
          $ r Y nw xY wdS )zCReturn Slack Web API error code from SlackApiError-like exceptions.responseNr   )rf   r   r   r*   r(   r   )r   r   r7   s      r   _slack_api_error_coder      s    uj$//H(D!! -W%%",s5zzz,	LL))E!&03u:::D0 	 	 	D	4s   'A: :
BBc           
      8	  K   t          | dd          pi }|s t          j        t          d           d{V S g }t	                      }|                                D ]\  }}	 d}t          d          D ]E}|                    ddd|           d{V }|                    d	          sO|                    d
d          }	|	dk    rt          
                    d|           nd|	 }
t          ||
            n|                    dg           D ]z}|                    d          }|                    d          }|r|r||v r5|                    |           |                    |||                    d          rdndd           {|                    d          pi                     d          }|s nGa# t          $ rW}t          |          dk    rt          
                    d|           nt          |t!          |                     Y d}~d}~ww xY wd |D             }t          j        t          d           d{V D ]}}|                    d          }||vrb|                    dd                              d          r||v r||         |d<   |                    |           |                    |           ~d |D             }|r|rt%          t'          |                                                    }|D ]}	 |                    |d                    d{V }|                    d	          s<|                    di           }|                    d          r|                    dd          }|r|                    |           d{V }|                    d	          rk|d         }|                    d i                               d!          p1|                    d"          p|                    d          p|d         |d<   d#|d$<   n5|                    d          p|                    d%          p|d         |d<   S# t          $ r-}t          
                    d&|d         |           Y d}~d}~ww xY w|S )'a  List Slack channels the bot has joined across all workspaces.

    Uses ``users.conversations`` against each workspace's web client. Pulls
    public + private channels the bot is a member of, then merges in DMs
    discovered from session history (IMs aren't useful to enumerate
    proactively). If the Slack app lacks channels:read, fall back to session
    history quietly instead of logging a recurring warning every refresh.
    _team_clientsNra      zpublic_channel,private_channelT   )typesexclude_archivedlimitcursorokr   unknownmissing_scopezPChannel directory: Slack team %s lacks channels:read; using session history onlyzusers.conversations not ok: r   r   r   
is_privateprivater?   )r   r   r#   response_metadatanext_cursorc                 ,    i | ]}|d          |d         S )r   r    rg   r   s     r   
<dictcomp>z _build_slack.<locals>.<dictcomp>E  s"    ???r$xF???r6    C0D0G0c                 d    g | ]-}|                     d d                              d          +|.S )r   r   r   )r*   
startswithr   s     r   
<listcomp>z _build_slack.<locals>.<listcomp>T  s:    ]]]266&"+=+=+H+HI[+\+\]"]]]r6   )r?   is_imuser)r   profiledisplay_name	real_namer!   r#   name_normalizedz+Channel directory: failed to resolve %s: %s)rf   ro   rp   ru   setr%   rangeusers_conversationsr*   rW   rY   r]   addr+   r   r   r(   r   nextitervaluesconversations_info
users_info)r{   team_clientsr   seen_idsrQ   r   r   _pager   
error_coderR   r   cidr   r4   api_name_lookupr   eid
unresolvedrespch_info	peer_user	user_respus                           r   rs   rs     s      7OT::@bL F&';WEEEEEEEEE%'HEEH'--// + +*	$(Fr  !'!;!;:%)!	 "< " "        ||D)) 
!)gy!A!AJ!_44n#   
 "M
!L!L-gv>>>E",,z266 
 
B&&,,C66&>>D !d !cXoo LL%%%OO! $-/VVL-A-A P		y% %    
 #,,':;;ArFF}UU E 	 	 	$Q''?::f   
 &gs1vv666HHHH	 @?h???O()=wGGGGGGGG 	 	iiooh yy$$//0BCC 9/))$3C$8E&MOOE"""LL ^]x]]]J l d<..001122 	 	E#66uT{6KKKKKKKKxx~~ ((9b11;;w'' i 'FB 7 7I  
1*0*;*;*;*K*K$K$K$K$K$K$K	$==.. 1 )& 1A !i 4 4 8 8 H H !/#$55#5#5!/#$55==!/ $);	 "&M -1E&M$+KK$7$7$h7;;GX;Y;Y$h]bcg]hE&M   JERVKYZ[[[ Os9   #EF==
HAHH7Q 	DQ  
R*"RRc                 F    t          |           }|r|S t          |           S )zPull known channels/contacts from gateway session origin data.

    state.db is the primary source (#9006): gateway session rows persist
    origin_json.  Falls back to sessions.json for pre-migration databases.
    )_build_from_sessions_db_build_from_sessions_json)r>   r0   s     r   ru   ru   s  s,     &m44G $]333r6   c           	         g }	 ddl m}  |            }	 t          |dd          }t          |          sg |                                 S  || d          }|                                 n# |                                 w xY wt                      }|D ]}i }|                    d          rJ	 t          j        |d                   }	t          |	t                    r|	}n# t          t          f$ r Y nw xY w|s?|                    d          |                    d	          |                    d
          d}t          |          }
|
r|
|v r|                    |
           |                    |
t!          |          |                    d          pd|                    d	          d           !n3# t"          $ r&}t$                              d| |           Y d}~nd}~ww xY w|S )z:Pull channels/contacts from state.db gateway session rows.r   )	SessionDBlist_gateway_sessionsNF)rz   active_onlyorigin_jsonr1   r$   r   )r1   r$   rK   	chat_typer!   r"   z:Channel directory: state.db session read failed for %s: %s)hermes_stater   rf   callablecloser   r*   r   loadsr   r   	TypeError
ValueErrorrI   r   r+   rP   r   rW   rY   )r>   r0   r   dblisterrowsr   rowrE   parsedentry_idr4   s               r   r   r     sS   $&G)
******Y[[	R!8$??FF##  HHJJJJ 6=eDDDDHHJJJJBHHJJJJ55 	 	C%'Fww}%% !ZM(:;;F!&$// (!'!:.   D "wwy11!$!5!5!$!8!8 
 )00H x833LL"""NN+F33,,4#ZZ44	     '	2  
 
 
H1	
 	
 	
 	
 	
 	
 	
 	


 Ns^   F8 !A- F8 A- F8 -B-F8 11C#"F8 #C74F8 6C77C F8 8
G(G##G(c           	      V   t                      dz  dz  }|                                sg S g }	 t          |d          5 }t          j        |          }ddd           n# 1 swxY w Y   t                      }|                                D ]\  }}t          |                              d          st          |t                    s=|                    d          pi }|                    d          | k    rnt          |          }	|	r|	|v r|                    |	           |                    |	t          |          |                    d	d
          |                    d          d           n3# t           $ r&}
t"                              d| |
           Y d}
~
nd}
~
ww xY w|S )zGLegacy fallback: pull channels/contacts from sessions.json origin data.sessionszsessions.jsonr   r   N_rE   rz   r   r!   r$   r"   z5Channel directory: failed to read sessions for %s: %s)r   r   r   r   r   r   r%   r(   r   r   r   r*   rI   r   r+   rP   r   rW   rY   )r>   sessions_pathr0   r   r   r   _keysessionrE   r   r4   s              r   r   r     s   #%%
2_DM!! 	G`-'222 	 a9Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  55!ZZ\\ 	 	MD' 4yy##C(( 
7D0I0I [[**0bFzz*%%66(00H x833LL"""NN+F33K66#ZZ44	     	$  ` ` `Lm]^________` Ns;   E6 A E6  A$$E6 'A$(DE6 6
F& F!!F&c                     t                                           sdi d} t          | d                    | S 	 t          t           d          5 }t	          j        |          }ddd           n# 1 swxY w Y   t          |                    di                      |S # t          $ r di d} t          | d                    | cY S w xY w)z,Load the cached channel directory from disk.Nrk   r   r   r   )ry   r   r5   r   r   r   r&   r   )baser   r   s      r   load_directoryr     s     "" "44tK0111
.7333 	 q9Q<<D	  	  	  	  	  	  	  	  	  	  	  	  	  	  	  	t{B??@@@   "44tK0111s5   B A."B .A22B 5A26'B &CCr1   c                     t                      }|                    di                               | g           D ]2}|                    d          |k    r|                    d          c S 3dS )ziReturn the channel ``type`` string (e.g. ``"channel"``, ``"forum"``) for *chat_id*, or *None* if unknown.r   r   r#   N)r   r*   )r>   r1   r   r   s       r   lookup_channel_typer     so      ImmK,,00CC " "66$<<7""66&>>!!! #4r6   r   c                 $  
 t                      }|                    di                               | g           }|sdS |                                }|D ]%}|                    d          |k    r
|d         c S &t          |          
|D ]P}t          |d                   
k    r
|d         c S t          t	          | |                    
k    r
|d         c S Qd
v r
                    dd          \  }}|D ]e}|                    dd                                                                          }||k    r#t          |d                   |k    r
|d         c S f
fd	|D             }	t          |	          dk    r|	d
         d         S dS )a  
    Resolve a human-friendly channel name to a numeric ID.

    Matching strategy (case-insensitive, first match wins):
    - Discord: "bot-home", "#bot-home", "GuildName/bot-home"
    - Telegram: display name or group name
    - Slack: "engineering", "#engineering"
    r   Nr   r   /   rB   r   c                 d    g | ],}t          |d                                                  *|-S )r   )r=   r   )rg   r   querys     r   r   z(resolve_channel_name.<locals>.<listcomp>  s:    ___b(@F(L(L(W(WX](^(^_r___r6   r   )r   r*   r)   r=   rD   rsplitr;   len)r>   r   r   r   rawr   
guild_partch_partrB   matchesr   s             @r   resolve_channel_namer     s      I}}["--11-DDH t
 **,,C  66$<<3d8OOO  %T**E   #BvJ//588d8OOO#$8$K$KLLPUUUd8OOO V e||#ll322
G 	  	 BFF7B''--//5577E
""'?6
'K'Kw'V'V$x `___H___G
7||qqz$4r6   c                     t                      } |                     di           }t          |                                          sdS dg}t	          |                                          D ]\  }}|s	|dk    r3i }g }|D ]X}|                    d          }|r*|                    |g                               |           C|                    |           Yt	          |                                          D ]Y\  }	}
|                    d|	 d           t	          |
d 	          D ](}|                    d
t          ||                      )Z|r@|                    d           |D ](}|                    d
t          ||                      )|                    d           C|                    |	                                 d           |D ]+}|                    d| dt          ||                      ,|                    d           |                    d           |                    d           d
                    |          S )zDFormat the channel directory as a human-readable list for the model.r   z?No messaging platforms connected or no channels discovered yet.zAvailable messaging targets:
rA   rB   z	Discord (z):c                     | d         S )Nr   r   )cs    r   <lambda>z.format_directory_for_display.<locals>.<lambda>=  s
    qy r6   )rZ   z
  discord:zDiscord (DMs):r   rG   z  z1Use these as the "target" parameter when sending.z;Bare platform name (e.g. "telegram") sends to home channel.
)r   r*   anyr   sortedr%   r&   r+   rD   titlejoin)r   r   linesr.   r   r   dmsr   rB   
guild_nameguild_channelss              r   format_directory_for_displayr  "  s     Ik2..Iy!!"" QPP-.E%ioo&7&788  	8 	 	!!&(FC # #w #%%eR0077;;;;JJrNNNN.4V\\^^.D.D U U*
N7777888 5H5HIII U UBLL!S.B9b.Q.Q!S!STTTTU U-... U UBLL!S.B9b.Q.Q!S!STTTTLLLLIOO--000111 U US)SS.B9b.Q.QSSTTTTLL	LLDEEE	LLNOOO99Ur6   )-__doc__ro   r   loggingrT   r   typingr   r   r   r   hermes_cli.configr   utilsr	   	getLogger__name__rW   ry   rV   r
   tupler(   float__annotations__r   r   r5   r=   rD   rI   rP   r]   r   rq   r   r   rs   ru   r   r   r   r   r   r  r   r6   r   <module>r     s               , , , , , , , , , , , , - - - - - - # # # # # #		8	$	$ ""%== -1 )>@ tE#s(OU$:; @ @ @ '((+AA tCc3h$78    d38n     B-C -C - - - - d38n     d38n #    *S#X *3 * * * *
3 
 
 
 
 
 
2ADcN AtCH~ A A A AH"tDcN3 " " " "J x}    j4S#X#7 j j j jZ	4 	4T#s(^0D 	4 	4 	4 	4-3 -4S#X3G - - - -`!S !T$sCx.5I ! ! ! !PS#X    &s S Xc]    , ,3 ,8C= , , , ,^+c + + + + + +r6   