
    JjF                        d 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"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.jsonz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/rurouni/.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_aliasesr4   (   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)r6   s    r   _normalize_channel_queryr;   I   s,    <<""$$**,,,r5   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guildr8   r"   z ())r)   )r<   r=   r   s      r   _channel_target_namerB   M   so    6?D	!!gkk'&:&:!4zz	!!gkk&&9&9!,,'&/,,,,Kr5   originc                     |                      d          }|sd S |                      d          }|r| d| S t          |          S )Nr0   r#   :r)   r'   )rC   r0   r#   s      r   _session_entry_idrG   W   sY    jj##G t

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

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

;''I **\**B.By.B.BK))K)))r5   adaptersc                 l  K   ddl m} i }|                                 D ]\  }}	 ||j        k    rt	          |          |d<   n#||j        k    rt          |           d{V |d<   H# t          $ r+}t          	                    d|j
        |           Y d}~xd}~ww xY wt          h d          }|D ]$}|j
        }||v s||v rt          |          ||<   %	 ddlm}	 |	                                D ]0}
|
j        |vr%|
j        |vrt          |
j                  ||
j        <   1n# t          $ r Y nw xY wt#          |           t%          j                                                    |d	}	 t+          t,          |           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   )Platformr?   Nslackz)Channel directory: failed to build %s: %s>   localwebhook
api_server)platform_registry
updated_atr   z&Channel directory: failed to write: %s)gateway.configrQ   r$   DISCORD_build_discordSLACK_build_slackr   loggerwarningr6   	frozenset_build_from_sessionsgateway.platform_registryrV   plugin_entriesr   r4   r   now	isoformatr	   DIRECTORY_PATH)rO   rQ   r   platformadapterr3   _SKIP_SESSION_DISCOVERYplatr-   rV   entry	directorys               r   build_channel_directoryrm   o   sl      ('''''13I%^^-- [ ['	[8+++'5g'>'>	)$$X^+++7+@+@%@%@%@%@%@%@	'" 	[ 	[ 	[NNFXYZZZZZZZZ	[ ((J(J(JKK ? ?J	///9	3I3I3I>>	)??????&5577 	I 	IEz!888UZy=X=X(<UZ(H(H	%*%	I     9%%% lnn..00 I
D.)4444 D D D?CCCCCCCCD sC   AA''
B1!BBAD$ $
D10D1,F 
F1F,,F1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   r@   r"   forum_channelsforumr?   )getattrr?   ImportErrorguildstext_channelsr*   r'   r   r   extendra   )rh   channelsclient_discordr@   chforumss          r   r[   r[      sK   HWi..F """""      % 	 	BOO"%jj!	       0$77=2 	 	BOO"%jj	     	 OO(33444Os    --c           
      V  K   t          | dd          pi }|st          d          S g }t                      }|                                D ]v\  }}	 d}t	          d          D ](}|                    ddd|           d{V }|                    d	          s2t                              d
||                    dd                      n|                    dg           D ]z}	|	                    d          }
|	                    d          }|
r|r|
|v r5|	                    |
           |
                    |
||	                    d          rdndd           {|                    d          pi                     d          }|s n*D# t          $ r'}t                              d||           Y d}~pd}~ww xY wt          d          D ]V}|                    d          |vr=|
                    |           |	                    |                    d                     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).
    _team_clientsNrR      zpublic_channel,private_channelT   )typesexclude_archivedlimitcursorokz=Channel directory: users.conversations not ok for team %s: %serrorunknownrw   r   r   
is_privateprivater=   )r   r   r"   response_metadatanext_cursorz@Channel directory: failed to list Slack channels for team %s: %s)rr   ra   setr$   rangeusers_conversationsr)   r^   r_   addr*   r   )rh   team_clientsrw   seen_idsteam_idrx   r   _pageresponserz   cidr   r3   rk   s                 r   r]   r]      s      7OT::@bL -#G,,,%'HEEH'--// $ $#	$(Fr  !'!;!;:%)!	 "< " "        ||D)) NNW Wi88  
 E",,z266 
 
B&&,,C66&>>D !d !cXoo LL%%%OO! $-/VVL-A-A P		y% %    
 #,,':;;ArFF}UU E 	 	 	NNR   HHHH	 &g.. * *99T??(**OOE"""LL4)))Os   D;F
G F;;G 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<   r/   s     r   ra   ra   	  s,     &m44G $]333r5   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)rg   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   rr   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     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_rC   rg   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   E  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.NrW   r   r   r   )rf   r   r4   r   r   r   r%   r   )baser   r   s      r   load_directoryr   m  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r0   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<   r0   rl   rz   s       r   lookup_channel_typer     so      ImmK,,00CC " "66$<<7""66&>>!!! #4r5   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   /   r@    c                 d    g | ],}t          |d                                                  *|-S )r   )r;   r   ).0rz   querys     r   
<listcomp>z(resolve_channel_name.<locals>.<listcomp>  s:    ___b(@F(L(L(W(WX](^(^_r___r5   r   )r   r)   r(   r;   rB   rsplitr:   len)r<   r   rl   rw   rawrz   
guild_partch_partr@   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$4r5   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:
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)rl   r   linesr-   rw   rt   dmsrz   r@   
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r5   )#__doc__r   loggingr   typingr   r   r   r   hermes_cli.configr   utilsr	   	getLogger__name__r^   rf   r   r'   r   r4   r;   rB   rG   rN   rm   r[   r]   ra   r   r   r   r   r   r   r   r5   r   <module>r      sr            , , , , , , , , , , , , - - - - - - # # # # # #		8	$	$ ""%== '((+AA tCc3h$78    d38n     B-C -C - - - - d38n     d38n #    *S#X *3 * * * *4DcN 4tCH~ 4 4 4 4n"tDcN3 " " " "J;4S#X#7 ; ; ; ;|	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 + + + + + +r5   