
    `gj?I                     \   d 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 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eef   d	eeef   fdZd	eeeef      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#y)"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.jsonzchannel_aliases.jsonreturnc                      t         j                         si S 	 t        t         d      5 } t        j                  |       }d d d        t        t              r|S i S # 1 sw Y   xY w# 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     L/root/.hermes/venv/lib/python3.12/site-packages/gateway/channel_directory.py_load_channel_aliasesr      sp    &&(	&9 	 Q99Q<D	 !$-t525	  	   	s-   A' AA' A' A$ A' 'A54A5	platformsc           	      .   t               }|j                         D ]  \  }}t        |t              s| j	                  |g       }t        |t
              s:|j                         D ]  \  }}t        |t              r|j                         s't        |      }|j                         }d}|D ]/  }t        |t              s|j                  d      |k(  s)||d<   d}1 |r{|j                  ||t        |      j                  d      rdnddd	         y)
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_aliasesr4   )   s    $%G$]]_ 	6&$'&&y"5'4(!' 	GXh,HNN4D'lG~~'HG #a&155;'+A (AfI"G# !$'*7|'<'<W'EG4!%	  	    valuec                 \    | j                  d      j                         j                         S )N#)lstripr(   lower)r6   s    r   _normalize_channel_queryr;   J   s#    <<""$**,,r5   platform_namechannelc                     |d   }| dk(  r|j                  d      rd| S | dk7  r|j                  d      r| d|d    dS |S )zHReturn the human-facing target label shown to users for a channel entry.r   discordguildr8   r"   z ())r)   )r<   r=   r   s      r   _channel_target_namerB   N   sZ    6?D	!gkk'&:4&z	!gkk&&9r'&/*!,,Kr5   originc                 t    | j                  d      }|sy | j                  d      }|r| d| S t        |      S )Nr0   r#   :r)   r'   )rC   r0   r#   s      r   _session_entry_idrG   X   sB    jj#G

;'I!I;''w<r5   c                     | j                  d      xs- | j                  d      xs t        | j                  d            }| j                  d      }|s|S | j                  d      xs d| }| d| S )N	chat_name	user_namer0   r#   
chat_topicztopic z / rF   )rC   	base_namer#   topic_labels       r   _session_entry_namerN   b   st    

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

;'I**\*Byk.BK[K=))r5   adaptersc           
      ~  K   ddl m} i }| j                         D ]`  \  }}	 ||j                  k(  r&t	        j
                  t        |       d{   |d<   n%||j                  k(  rt        |       d{   |d<   b t        h d      }| D ch c]  }t        |dt        |             }}|D ]@  }	|	j                  }
|
|v s|
|v s|
|vrt	        j
                  t         |
       d{   ||
<   B 	 dd	lm} |j'                         D ]h  }|j(                  |vs|j(                  |vs!|j(                  |v s0t	        j
                  t         |j(                         d{   ||j(                  <   j 	 t+        |       t-        j.                         j1                         |d
}	 t3        t4        |       |S 7 k7 H# t        $ r,}t        j                  d|j                  |       Y d}~d}~ww xY wc c}w 7 7 # t        $ r Y w xY w# t        $ r!}t        j                  d|       Y d}~|S d}~ww xY ww)z
    Build a channel directory from connected platform adapters and session data.

    Returns the directory dict and writes it to DIRECTORY_PATH.
    r   )PlatformNr?   slackz)Channel directory: failed to build %s: %s>   localwebhook
api_serverr6   )platform_registry
updated_atr   z&Channel directory: failed to write: %s)gateway.configrQ   r$   DISCORDasyncio	to_thread_build_discordSLACK_build_slackr   loggerwarningr6   	frozensetgetattrr'   _build_from_sessionsgateway.platform_registryrV   plugin_entriesr   r4   r   now	isoformatr	   DIRECTORY_PATH)rO   rQ   r   platformadapterr3   _SKIP_SESSION_DISCOVERYpadapter_platform_namesplatr-   rV   entry	directorys                 r   build_channel_directoryrr   p   s-     (13I%^^- ['	[8+++-4->->~w-W'W	)$X^^++7+@%@	'"[ ((JKCKLaga#a&9LL XJJ	00I% 66%,%6%67KY%WW	)X
?&557 	bE

"99JJi/JJ"88.5.?.?@TV[V`V`.a(a	%**%	b 9% lln..0I
D.)4 i (X%@ 	[NNFXYZZ	[ M  X )b   D?CCDs   H=,F?F9%F?4F<5F?<H=G7,=H=)G<*	H=4'H H +H :'H !G?"H 51H='H 7H=9F?<F??	G4!G/)H=/G44	H=?H 	H
H=HH=	H:H5/H=5H::H=c           	         g }t        | dd      }|s|S 	 ddl}|j                  D ]  }|j                  D ]?  }|j                  t        |j                        |j                  |j                  dd       A t        |dd      xs g }|D ]?  }|j                  t        |j                        |j                  |j                  dd       A  |j                  t        d             |S # t        $ r |cY S w xY w)	zGEnumerate all text channels and forum channels the Discord bot can see._clientNr   r=   )r   r   r@   r"   forum_channelsforumr?   )rc   r?   ImportErrorguildstext_channelsr*   r'   r   r   extendrd   )rk   channelsclient_discordr@   chforumss          r   r]   r]      s    HWi.F"  %% 	BOO"%%j!	 	  0$7=2 	BOO"%%j	 	* OO(34O3  s   C) )C76C7c           
      "  K   t        | dd      xs i }|s"t        j                  t        d       d{   S g }t	               }|j                         D ]  \  }}	 d}t        d      D ]  }|j                  ddd|       d{   }|j                  d	      s)t        j                  d
||j                  dd              n|j                  dg       D ]f  }	|	j                  d      }
|	j                  d      }|
r|r|
|v r.|j                  |
       |j                  |
||	j                  d      rdndd       h |j                  d      xs i j                  d      }|r n  t        j                  t        d       d{   D ]G  }|j                  d      |vs|j                  |       |j                  |j                  d             I |S 7 7 X# t        $ r"}t        j                  d||       Y d}~d}~ww xY w7 w)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).
    _team_clientsNrR      zpublic_channel,private_channelT   )typesexclude_archivedlimitcursorokz=Channel directory: users.conversations not ok for team %s: %serrorunknownr{   r   r   
is_privateprivater=   )r   r   r"   response_metadatanext_cursorz@Channel directory: failed to list Slack channels for team %s: %s)rc   r[   r\   rd   setr$   rangeusers_conversationsr)   r`   ra   addr*   r   )rk   team_clientsr{   seen_idsteam_idr|   r   _pageresponser~   cidr   r3   rp   s                 r   r_   r_      s     7OT:@bL&&';WEEE%'HEH'--/ $#	$(Fr !'!;!;:%)!	 "< "   ||D)NNW Wi8
 ",,z26 
B&&,C66&>DdcXo LL%OO! $-/VVL-A	y% 
 #,,':;ArFF}U7$N (()=wGG *99T?(*OOE"LL4)*
 Oc F6  	NNR 	 Hsd   1HG'H(GGCG%G( HH	H$6HG	H
(H?HH

Hc                 6    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<   r/   s     r   rd   rd     s!     &m4G$]33r5   c           	      2   g }	 ddl m}  |       }	 t        |dd      }t        |      sg |j	                          S  || d      }|j	                          t               }|D ]  }i }|j                  d      r+	 t        j                  |d         }	t        |	t              r|	}|s3|j                  d      |j                  d	      |j                  d
      d}t        |      }
|
r|
|v r|j                  |
       |j                  |
t!        |      |j                  d      xs d|j                  d	      d        	 |S # |j	                          w xY w# t        t        f$ r Y w xY w# t"        $ r"}t$        j'                  d| |       Y d}~|S d}~ww xY w)z:Pull channels/contacts from state.db gateway session rows.r   )	SessionDBlist_gateway_sessionsNF)rj   active_onlyorigin_jsonr0   r#   display_name)r0   r#   rI   	chat_typer    r!   z:Channel directory: state.db session read failed for %s: %s)hermes_stater   rc   callablecloser   r)   r   loadsr   r   	TypeError
ValueErrorrG   r   r*   rN   r   r`   debug)r<   r/   r   dblisterrowsr   rowrC   parsedentry_idr3   s               r   r   r   #  s   $&G)
*[	R!8$?FF# HHJ =eDDHHJ5 	C%'Fww}%!ZZM(:;F!&$/!' "wwy1!$!5!$!8
 )0Hx83LL"NN+F3,4#ZZ4	 '	< NC HHJ ":. $  
H1	
 	
 N
s]   E+ E E+ 
E 2E+ 9*E#BE+ EE+ E(%E+ 'E((E+ +	F4FFc           	         t               dz  dz  }|j                         sg S g }	 t        |d      5 }t        j                  |      }ddd       t               }j                         D ]  \  }}t        |      j                  d      st        |t              s1|j                  d      xs i }|j                  d      | k7  r[t        |      }	|	r|	|v rm|j                  |	       |j                  |	t        |      |j                  d	d
      |j                  d      d        	 |S # 1 sw Y   xY w# t         $ r"}
t"        j%                  d| |
       Y d}
~
|S d}
~
ww xY w)zGLegacy fallback: pull channels/contacts from sessions.json origin data.sessionszsessions.jsonr   r   N_rC   rj   r   r    r#   r!   z5Channel directory: failed to read sessions for %s: %s)r   r   r   r   r   r   r$   r'   
startswithr   r   r)   rG   r   r*   rN   r   r`   r   )r<   sessions_pathr/   r   r   r   _keysessionrC   r   r3   s              r   r   r   S  sT   #%
2_DM!	G`-'2 	 a99Q<D	  5!ZZ\ 	MD' 4y##C(
7D0I[[*0bFzz*%6(0Hx83LL"NN+F3K6#ZZ4	 	* N3	  	 ,  `Lm]^__N`s/   D8 D,	C D8 ,D51D8 8	E#EE#c                  N   t         j                         sdi d} t        | d          | S 	 t        t         d      5 }t	        j
                  |      }ddd       t        j                  di              |S # 1 sw Y   &xY w# t        $ r di d} t        | d          | cY S w xY w)z,Load the cached channel directory from disk.NrW   r   r   r   )ri   r   r4   r   r   r   r%   r   )baser   r   s      r   load_directoryr   {  s      ""4tK01
.73 	 q99Q<D	  	t{B?@	  	   "4tK01s(   B A7$B 7B <B B$#B$r0   c                     t               }|j                  di       j                  | g       D ])  }|j                  d      |k(  s|j                  d      c S  y)ziReturn the channel ``type`` string (e.g. ``"channel"``, ``"forum"``) for *chat_id*, or *None* if unknown.r   r   r"   N)r   r)   )r<   r0   rq   r~   s       r   lookup_channel_typer     sT     ImmK,00C "66$<7"66&>!" r5   r   c                    t               }|j                  di       j                  | g       }|sy|j                         }|D ]  }|j                  d      |k(  s|d   c S  t        |      }|D ]9  }t        |d         |k(  r|d   c S t        t	        | |            |k(  s4|d   c S  d|v rh|j                  dd      \  }}|D ]N  }|j                  dd      j                         j                         }	|	|k(  s7t        |d         |k(  sI|d   c S  |D cg c]"  }t        |d         j                  |      s!|$ }
}t        |
      dk(  r|
d	   d   S yc c}w )
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   /   r@    r   )	r   r)   r(   r;   rB   rsplitr:   r   len)r<   r   rq   r{   rawr~   query
guild_partch_partr@   matchess              r   resolve_channel_namer     s|     I}}["-11-DH
 **,C 66$<3d8O %T*E  #BvJ/58d8O#$8$KLPUUd8O	 e|#ll32
G 	 BFF7B'--/557E
"'?6
'Kw'V$x	  %_b(@F(L(W(WX](^r_G_
7|qqz$	 `s   "E>Ec                     t               } | j                  di       }t        |j                               sydg}t	        |j                               D ]m  \  }}|s
|dk(  ri }g }|D ]H  }|j                  d      }|r"|j                  |g       j                  |       8|j                  |       J t	        |j                               D ]K  \  }	}
|j                  d|	 d       t	        |
d 	      D ]   }|j                  d
t        ||              " M |r6|j                  d       |D ]   }|j                  d
t        ||              " |j                  d       |j                  |j                          d       |D ]#  }|j                  d| dt        ||              % |j                  d       p |j                  d       |j                  d       dj                  |      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:
r?   r@   z	Discord (z):c                     | d   S )Nr    )cs    r   <lambda>z.format_directory_for_display.<locals>.<lambda>  s
    qy r5   )keyz
  discord:zDiscord (DMs):r   rE   z  z1Use these as the "target" parameter when sending.z;Bare platform name (e.g. "telegram") sends to home channel.
)r   r)   anyvaluessortedr$   r%   r*   rB   titlejoin)rq   r   linesr-   r{   rx   dmsr~   r@   
guild_nameguild_channelss              r   format_directory_for_displayr     s    Ik2.Iy!"P-.E%ioo&78 	8 	!&(FC #w%%eR077;JJrN# /5V\\^.D U*
NyB78 5HI UBLL:.B9b.Q-R!STUU -. UBLL:.B9b.Q-R!STULLLLIOO-.a01 Ur)A.B9b.Q-RSTULL9< 
LLDE	LLNO99Ur5   )$__doc__r[   r   loggingr   typingr   r   r   r   hermes_cli.configr   utilsr	   	getLogger__name__r`   ri   r   r'   r   r4   r;   rB   rG   rN   rr   r]   r_   rd   r   r   r   r   r   r   r   r5   r   <module>r      s       , , - #			8	$ "%== '(+AA tCc3h$78 d38n  B-C -C - d38n  d38n # *S#X *3 *ADcN AtCH~ AH"tDcN3 "J;4S#X#7 ;|	4 	4T#s(^0D 	4-3 -4S#X3G -`!S !T$sCx.5I !PS#X &s S Xc] , ,3 ,8C= ,^+c +r5   