
    `gj                    N   d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZmZmZmZmZ ddlmZmZ ddlmZ ddlmZmZmZmZ  ej4                  e      Zd	Zd
ddddZdZdZ dZ!ddZ"ddZ# G d de      Z$dddd	 	 	 	 	 	 	 	 	 	 	 ddZ%d dZ&d!dZ'ddZ(d Z)d dZ*y)"u  Mattermost gateway adapter.

Connects to a self-hosted (or cloud) Mattermost instance via its REST API
(v4) and WebSocket for real-time events.  No external Mattermost library
required — uses aiohttp which is already a Hermes dependency.

Environment variables:
    MATTERMOST_URL              Server URL (e.g. https://mm.example.com)
    MATTERMOST_TOKEN            Bot token or personal-access token
    MATTERMOST_ALLOWED_USERS    Comma-separated user IDs
    MATTERMOST_HOME_CHANNEL     Channel ID for cron/notification delivery
    )annotationsN)Path)AnyDictListOptionalTuple)PlatformPlatformConfig)MessageDeduplicator)BasePlatformAdapterMessageEventMessageType
SendResulti  dmgroupchannel)DGPOg       @g      N@g?c                 V    	 ddl } y# t        $ r t        j                  d       Y yw xY w)zFReturn True if the Mattermost adapter runtime dependency is available.r   NTz!Mattermost: aiohttp not installedF)aiohttpImportErrorloggerwarning)r   s    W/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/mattermost/adapter.pycheck_mattermost_requirementsr   5   s,     :;s    ((c                \   t        | di       xs i }t        | dd      xs t        j                  dd      j                         }|j	                  dd      xs t        j                  dd      j                         }|st
        j                  d       y	|st
        j                  d
       y	y)z9Return True when Mattermost has enough config to connect.extratokenNMATTERMOST_TOKEN urlMATTERMOST_URLz$Mattermost: MATTERMOST_TOKEN not setFz"Mattermost: MATTERMOST_URL not setT)getattrosgetenvstripgetr   debugr   )configr    r!   r$   s       r   validate_mattermost_configr-   ?   s    FGR(.BEVWd+Pryy9KR/PWWYE99UBB299-=r#B
I
I
KC;<;<    c                      e Zd ZdZdZd" fdZd#dZd$dZ	 	 	 	 	 	 d%dZ	 	 	 	 	 	 d&dZ	d'dZ
	 	 	 	 	 	 	 	 d(d	Z	 	 	 	 	 	 d%d
Z	 d)	 	 	 	 	 	 	 	 	 d*dZddd+dZd,dZd-dZ	 	 d.	 	 	 	 	 	 	 	 	 d/dZd0dZ	 d1	 	 	 	 	 d2dZdd	 	 	 	 	 	 	 	 	 d3dZ	 	 	 d4	 	 	 	 	 	 	 	 	 	 	 d5dZ	 	 	 d4	 	 	 	 	 	 	 	 	 	 	 d6dZ	 	 	 	 d7	 	 	 	 	 	 	 	 	 	 	 	 	 d8dZ	 	 	 d4	 	 	 	 	 	 	 	 	 	 	 d9dZ	 	 	 d4	 	 	 	 	 	 	 	 	 	 	 d:dZd;dZ	 	 d<	 	 	 	 	 	 	 	 	 	 	 	 	 d=dZ	 	 d.	 	 	 	 	 	 	 	 	 	 	 	 	 d>dZ	 	 d?	 	 	 	 	 	 	 	 	 d@ fdZd,dZd,d ZdAd!Z  xZ!S )BMattermostAdapterz6Gateway adapter for Mattermost (self-hosted or cloud).Tc                N   t         |   |t        j                         |j                  j                  dd      xs t        j                  dd      j                  d      | _	        |j                  xs t        j                  dd      | _        d| _        d| _        d | _        d | _        d | _        d | _        d| _        |j                  j                  dd      xs t        j                  dd	      j'                         | _        d | _        d| _        t/               | _        y )
Nr$   r#   r%   /r"   F
reply_modeMATTERMOST_REPLY_MODEoff)super__init__r
   
MATTERMOSTr    r*   r'   r(   rstrip	_base_urlr!   _token_bot_user_id_bot_username_session_ws_ws_task_reconnect_task_closinglower_reply_mode_last_post_status_last_post_errorr   _dedup)selfr,   	__class__s     r   r7   zMattermostAdapter.__init__R   s    !4!45 LLUB' /yy)2.
&+ 	 "<<L2995G+L!#"$ "047; LL\2. 9yy0%8
%' 	
 15%' *+r.   c                &    d| j                    ddS )NBearer application/jsonAuthorizationzContent-Type)r;   rH   s    r   _headerszMattermostAdapter._headersu   s    &t{{m4.
 	
r.   c           	       K   ddl }d|v rt        j                  d|       i S | j                   d|j	                  d       }	 | j
                  j                  || j                         |j                  d      	      4 d{   }|j                  d
k\  rP|j                          d{   }t        j                  d||j                  |dd        i cddd      d{    S |j                          d{   cddd      d{    S 7 7 g7 27 7 # 1 d{  7  sw Y   yxY w# |j                  $ r#}t        j                  d||       i cY d}~S d}~ww xY ww)zGET /api/v4/{path}.r   N..!MM API path traversal blocked: %s/api/v4/r2      totalheaderstimeout  u   MM API GET %s → %s: %s   zMM API GET %s network error: %s)r   r   errorr:   lstripr>   r*   rP   ClientTimeoutstatustextjsonClientError)rH   pathr   r$   respbodyexcs          r   _api_getzMattermostAdapter._api_get{   s@    4<LL<dCI S)9(:;		}}((dmmowOdOdkmOdOn(o ) )sw;;#%!%,DLL!;T4;;PTUYVYPZ[	) ) )
 "YY[() ) ),)
 )) ) ) ) "" 	LL:D#FI	s   AE%A D0 DD0 #D+D,*DD0 "D#D0 'E%(D;D<D?D0 DD0 E%D0 DD0 DD0 D-!D$"D-)D0 ,E%-D0 0E"?EE"E%E""E%c           	     D  K   ddl }d|v rt        j                  d|       i S | j                   d|j	                  d       }d| _        d| _        	 | j                  j                  || j                         ||j                  d	      
      4 d{   }|j                  | _        |j                  dk\  r[|j                          d{   }|xs d| _        t        j                  d||j                  |dd        i cddd      d{    S |j                          d{   cddd      d{    S 7 7 r7 27 7 # 1 d{  7  sw Y   yxY w# |j                  $ r3}t        |      | _        t        j                  d||       i cY d}~S d}~ww xY ww)z#POST /api/v4/{path} with JSON body.r   NrR   rS   rT   r2   r#   rU   rV   )rY   rb   rZ   r[   u   MM API POST %s → %s: %sr\   z MM API POST %s network error: %s)r   r   r]   r:   r^   rE   rF   r>   postrP   r_   r`   ra   rb   rc   strrH   rd   payloadr   r$   re   rf   rg   s           r   	_api_postzMattermostAdapter._api_post   s     	4<LL<dCI S)9(:;!% "	}}))T]]_7--B-7 *  
) 
) )-&;;#%!%,D,0JBD)LL!<dDKKQUVZWZQ[\
) 
) 
) "YY[(
) 
) 
) -
) )
) 
) 
) 
) "" 	$'HD!LL;T3GI	s   AF AE D<E 4ED>5EE E E F E&E'E*E 6E7E ;F <E >E E EE EEEE F E F*(FFF FF c                   K   | j                   dk7  ry|}|s4t        |t              r$|j                  d      xs |j                  d      }|sy| j	                  t        |             d{   S 7 w)z9Resolve the Mattermost root_id from reply_to or metadata.threadN	thread_idroot_id)rD   
isinstancedictr*   _resolve_root_idrk   )rH   reply_tometadata	candidates       r   _thread_root_for_sendz'MattermostAdapter._thread_root_for_send   sg      x'	Z$7 [1LX\\)5LI**3y>::::s   A)A2+A0,A2c                    | j                   dvry| j                  xs dj                         syt        fddD              }t        fddD              }|xr |S )zCReturn True only for clear invalid/missing Mattermost thread roots.>     r[   Fr#   c              3  &   K   | ]  }|v  
 y wN .0markerrf   s     r   	<genexpr>zMMattermostAdapter._last_post_failure_is_broken_thread_root.<locals>.<genexpr>   s     ffnf   )rr   rootidzroot idrp   rj   c              3  &   K   | ]  }|v  
 y wr}   r~   r   s     r   r   zMMattermostAdapter._last_post_failure_is_broken_thread_root.<locals>.<genexpr>   s     hVt^hr   )invalidz	not foundzdoes not existmissing)rE   rF   rC   any)rH   rootishbrokenrf   s      @r   (_last_post_failure_is_broken_thread_rootz:MattermostAdapter._last_post_failure_is_broken_thread_root   s`    !!3%%+224f3effh2ghh!6!r.   c                  K   | j                  d|       d{   }|sd|vr|S t        |t              r|j                  d      s|S | j	                         s|S t        |      }|j                  dd       t        |j                  d      xs d      }d|z   j                         |d<   t        j                  d|       | j                  d|       d{   S 7 7 w)	zAPost once, optionally falling back flat for final notify content.postsNrr   notifymessager#   uK   ⚠️ Mattermost thread delivery failed; posting final reply in channel.

zNMattermost: falling back to flat channel delivery for notify-worthy post in %s)
rn   rs   rt   r*   r   poprk   r)   r   r   )rH   chat_idrm   rw   dataflat_payloadoriginals          r   _post_preserving_threadz)MattermostAdapter._post_preserving_thread   s      ^^GW559G+K8T*x||H/EK<<>KG}D)|''	28b9[
%' 	Y 	\	
 ^^G\:::' 6& ;s"   CCB=CCCCc           	       K   ddl }d|v rt        j                  d|       i S | j                   d|j	                  d       }	 | j
                  j                  || j                         |      4 d{   }|j                  dk\  rP|j                          d{   }t        j                  d	||j                  |dd
        i cddd      d{    S |j                          d{   cddd      d{    S 7 7 g7 27 7 # 1 d{  7  sw Y   yxY w# |j                  $ r#}t        j                  d||       i cY d}~S d}~ww xY ww)z"PUT /api/v4/{path} with JSON body.r   NrR   rS   rT   r2   rY   rb   r[   u   MM API PUT %s → %s: %sr\   zMM API PUT %s network error: %s)r   r   r]   r:   r^   r>   putrP   r`   ra   rb   rc   rl   s           r   _api_putzMattermostAdapter._api_put   s:     	4<LL<dCI S)9(:;	}}((T]]_7 )  ) );;#%!%,DLL!;T4;;PTUYVYPZ[) ) ) "YY[() ) ) -	) )) ) ) ) "" 	LL:D#FI	s   AE0D  4D5D  8#DD*DD  DD  ED+D,D/D  ;D	<D   ED  DD  D	D  DDDD  ED   E/EEEEEc                  K   ddl }| j                   d}|j                         }|j                  d|       |j                  d|||       dd| j                   i}| j
                  j                  ||||j                  d	
            4 d{   }	|	j                  dk\  rN|	j                          d{   }
t        j                  d|	j                  |
dd        	 ddd      d{    y|	j                          d{   }|j                  dg       }|r|d   d   ndcddd      d{    S 7 7 7 P7 97 # 1 d{  7  sw Y   yxY ww)z9Upload a file and return its file ID, or None on failure.r   N/api/v4/files
channel_idfiles)filenamecontent_typerN   rK   <   rV   )rY   r   rZ   r[   u   MM file upload → %s: %sr\   
file_infosid)r   r:   FormData	add_fieldr;   r>   rj   r_   r`   ra   r   r]   rb   r*   )rH   r   	file_datar   r   r   r$   formrY   re   rf   r   infoss                r   _upload_filezMattermostAdapter._upload_file   sN     	 .!|Z0%	 	 	
 #gdkk]$;<==%%c7wOdOdkmOdOn%o 	5 	5sw{{c!!YY[(8$++tDSzR		5 	5 	5
 $DHH\2.E%*58D>	5 	5 	5(	5
 %	5 	5 	5 	5s   BED:E#E8D<9)E"E-D>.E3EE !E(E4E5E<E>E EEE
EEEF)is_reconnectc                 K   ddl }| j                  r| j                  st        j	                  d       y|j                  |j                  d            | _        d| _        | j                  d       d{   }|rd	|vr8t        j	                  d
       | j                  j                          d{    y|d	   | _        |j                  dd      | _        t        j                  d| j                  | j                  | j                         t        j                   | j#                               | _        | j'                          y7 7 w)z7Connect to Mattermost and start the WebSocket listener.r   Nz'Mattermost: URL or token not configuredFrU   rV   rZ   zusers/mer   uP   Mattermost: failed to authenticate — check MATTERMOST_TOKEN and MATTERMOST_URLusernamer#   z+Mattermost: authenticated as @%s (%s) on %sT)r   r:   r;   r   r]   ClientSessionr_   r>   rB   rh   closer<   r*   r=   infoasynciocreate_task_ws_loopr@   _mark_connected)rH   r   r   mes       r   connectzMattermostAdapter.connect  s    ~~T[[LLBC--)))3 . 
  ==,,T^LLkl--%%'''tHVVJ39NN		
  ++DMMO<% - (s%   A5E7E
8<E4E5BEEc                  K   d| _         | j                  rI| j                  j                         s/| j                  j                          	 | j                   d{    | j                  r4| j                  j                         s| j                  j                          | j                  r)| j                  j                          d{    d| _        | j                  r8| j                  j                  s"| j                  j                          d{    t        j                  d       y7 # t        j
                  t        f$ r Y w xY w7 7 =w)zDisconnect from Mattermost.TNzMattermost: disconnected)rB   r@   donecancelr   CancelledError	ExceptionrA   r?   r   r>   closedr   r   rO   s    r   
disconnectzMattermostAdapter.disconnect4  s     ==!3!3!5MM  "mm## (<(<(A(A(C  '')88((.."""DH==!5!5--%%'''./ $**I6  # (s\   AED0 D.D0 A)EE	A
EEE.D0 0E	EEEEc                   K   |s|S | j                  d|        d{   }|r|j                  d      r|d   S |S 7 w)a=  Resolve a post_id to the thread root_id for Mattermost.

        Mattermost requires root_id to be the *root* post of a thread.
        If the post is a reply (has its own root_id), we must use that
        root_id instead.  Using a reply's own ID as root_id causes
        "Invalid RootId parameter" errors.
        posts/Nrr   )rh   r*   )rH   post_idr   s      r   ru   z"MattermostAdapter._resolve_root_idL  sJ      N]]VG9#566DHHY'	?" 7s   ?=?c                ^  K   |st        d      S | j                  |      }| j                  |t              }d}|D ]]  }||d}	| j	                  ||       d{   }
|
r|
|	d<   | j                  ||	|       d{   }|rd|vrt        dd	      c S |d   }_ t        d|
      S 7 O7 /w)z1Send a message (or multiple chunks) to a channel.T)successNr   r   rr   r   FzFailed to create postr   r]   r   
message_id)r   format_messagetruncate_messageMAX_POST_LENGTHry   r   )rH   r   contentrv   rw   	formattedchunkslast_idchunkrm   resolved_rootr   s               r   sendzMattermostAdapter.send\  s      d++''0	&&y/B 	!E% 'G
 #'"<"<Xx"PPM%2	"55gwQQD4t+!%7NOO4jG	! $7;; Q Rs$   AB-B)!B-;B+<.B-+B-c                   K   | j                  d|        d{   }|s|ddS t        j                  |j                  dd      d      }|j                  d      xs |j                  d      xs |}||dS 7 ^w)	zReturn channel name and type.z	channels/Nr   )nametyper   r   display_namer   )rh   _CHANNEL_TYPE_MAPr*   )rH   r   r   ch_typer   s        r   get_chat_infozMattermostAdapter.get_chat_info|  sz     ]]Ywi#899#Y77#''(=yIxx/N488F3CNw$g66 :s   A;A9AA;c                d   K   | j                  d| j                   dd|i       d{    y7 w)zSend a typing indicator.zusers/z/typingr   N)rn   r<   )rH   r   rw   s      r   send_typingzMattermostAdapter.send_typing  s9      nnT&&'w/7#
 	
 	
s   &0.0)finalizec                  K   | j                  |      }| j                  d| dd|i       d{   }|rd|vrt        dd      S t        d	|d   
      S 7 'w)zEdit an existing post.r   z/patchr   Nr   FzFailed to edit postr   Tr   )r   r   r   )rH   r   r   r   r   r   r   s          r   edit_messagezMattermostAdapter.edit_message  so      ''0	]]ZL'	"
 
 t4'e3HII$4:>>
s   -AA(Ac                J   K   | j                  ||||d|       d{   S 7 w)z5Download an image and upload it as a file attachment.imageN)_send_url_as_file)rH   r   	image_urlcaptionrv   rw   s         r   
send_imagezMattermostAdapter.send_image  s2      ++Y7H
 
 	
 
   #!#c                J   K   | j                  |||||       d{   S 7 w)zUpload a local image file.rw   N_send_local_file)rH   r   
image_pathr   rv   rw   s         r   send_image_filez!MattermostAdapter.send_image_file  5      **Z(X + 
 
 	
 
r   c                J   K   | j                  ||||||       d{   S 7 w)z"Upload a local file as a document.Nr   )rH   r   	file_pathr   	file_namerv   rw   s          r   send_documentzMattermostAdapter.send_document  s2      **Y9h
 
 	
 
r   c                J   K   | j                  |||||       d{   S 7 w)zUpload an audio file.r   Nr   )rH   r   
audio_pathr   rv   rw   s         r   
send_voicezMattermostAdapter.send_voice  r   r   c                J   K   | j                  |||||       d{   S 7 w)zUpload a video file.r   Nr   )rH   r   
video_pathr   rv   rw   s         r   
send_videozMattermostAdapter.send_video  r   r   c                4    t        j                  dd|      }|S )u   Mattermost uses standard Markdown — mostly pass through.

        Strip image markdown into plain links (files are uploaded separately).
        z!\[([^\]]*)\]\(([^)]+)\)z\2)resub)rH   r   s     r   r   z MattermostAdapter.format_message  s     &&4eWEr.   c           	       K   ddl m}  ||      sIt        j                  d       | j	                  ||xs d d| j                         ||       d{   S ddl}d}	d}
|j                  d	d
      d   j                  d      d   xs | d}t        d      D ]I  }	 | j                  j                  ||j                  d            4 d{   }|j                  dk\  s|j                  dk(  rb|dk  r]t        j                  d|d
z   |dd |j                         t        j                   d|d
z   z         d{    	 ddd      d{    |j                  dk\  rG| j	                  ||xs d d| j                         ||       d{   cddd      d{    c S |j#                          d{   }	|j$                  xs d}
	 ddd      d{     n |	Jt        j                  d|       | j	                  ||xs d d| j                         ||       d{   S | j+                  ||	||
       d{   }|s4| j	                  ||xs d d| j                         ||       d{   S ||xs d|gd}| j-                  ||       d{   }|r||d<   | j/                  |||       d{   }|rd|vrt1        dd       S t1        d!|d   "      S 7 7 "7 7 7 h7 [7 C7 %# 1 d{  7  sw Y   nxY w# |j&                  t        j(                  f$ r}|dk  r*t        j                   d|d
z   z         d{  7   Y d}~t        j                  d||d
z   |       | j	                  ||xs d d| j                         ||       d{  7  cY d}~c S d}~ww xY w7 7 7 Q7 /7 w)#z2Download a URL and upload it as a file attachment.r   is_safe_urlz0Mattermost: blocked unsafe URL (SSRF protection)r#   
r   Napplication/octet-streamr2      ?.png   rU   rV   r   i  i     z1Mattermost download retry %d/2 for %s (status %d)P   g      ?r[   z7Mattermost: failed to download %s after %d attempts: %sz,Mattermost: download returned no data for %sr   r   file_idsrr   r   FFailed to post with filer   Tr   )tools.url_safetyr   r   r   r   r)   r   rsplitsplitranger>   r*   r_   r`   r+   r   sleepreadr   rc   TimeoutErrorr   ry   r   r   )rH   r   r$   r   rv   kindrw   r   r   r   ctfnameattemptre   rg   file_idrm   r   r   s                      r   r   z#MattermostAdapter._send_url_as_file  s     	13NNMN7w}"oRu,E,K,K,Mxbjkkk	'

3"2&,,S1!4E4&Qx 	pGp==,,S':O:OVX:O:Y,Z  ^b{{c)T[[C-?"Q;"LL)\)01c#2hM")--w{0C"DDD$   {{c)%)YYw7=b/C58Q8W8W8Y[cnvY%ww   '+iik 1I**H.HB  	p* NNI3O7w}"oRu,E,K,K,Mxbjkkk))'9eRHH7w}"oRu,E,K,K,Mxbjkkk "}" 	#

 #888LL!.GI11'7HMMt4'e3MNN$4:>>c l
 E  x !2     '')=)=> pQ;!--w{(;<<<XZ]_fij_jlop!YYw7=b/C50I0O0O0QS[fnYoooop lHk M Ns  AOK0A	O 1LK3LA*L?K6 LLK9LO>LK<LL$K?%L)O,L?L LL L!L%A
O/O0OO5OO$O&O'!OO	(O3L6L9L<L?LLLL	LL	LOO	>#O!M$"O'O-A
O7N:8O<O	=OO		OOOOOc                4  K   ddl }t        |      }|j                         s#t        j	                  d|       t        dd      S |xs |j                  }	|j                  |	      d   xs d}
|j                         }| j                  |||	|
       d{   }|st        dd	      S ||xs d
|gd}| j                  ||       d{   }|r||d<   | j                  |||       d{   }|rd|vrt        dd	      S t        d|d         S 7 }7 K7 +w)z,Upload a local file and attach it to a post.r   Nz.Mattermost: local file not found, skipping: %sTr   r   FzFile upload failedr   r#   r  rr   r   r  )	mimetypesr   existsr   r   r   r   
guess_type
read_bytesr   ry   r   )rH   r   r   r   rv   r   rw   r  pr  r  r   r  rm   r   r   s                   r   r   z"MattermostAdapter._send_local_file0  s+     	OxxzNN@) dt<<#QVV!!%(+I/ILLN	))'9eRHHe3GHH "}" 	#

 #888LL!.GI11'7HMMt4'e3MNN$4:>>! I M Ns6   BDD3DD	!D*D+(DDDc           
       K   |syddl }ddl}ddlm} d}t	        dt        |      |      D 	cg c]
  }	||	|	|z     }
}	t        |
      D ]  \  }}|dkD  r"|dkD  rt        j                  |       d{    g }g }	 |D ]  \  }}|r|j                  |       |j                  d      rs ||dd       }t        |      }|j                         st        j                  d|       h|j                  }|j!                  |      d   xs d}|j#                         }ndd	lm}  ||      st        j                  d
       	 | j(                  j+                  ||j-                  d            4 d{   }|j.                  dk\  r7t        j                  d|j.                  |dd        	 ddd      d{    ?|j1                          d{   }|j2                  xs d}ddd      d{    |j7                  dd      d   j9                  d      d   xs dt        |       d}| j;                  ||       d{   }|s|j                  |        |s|dj=                  |      |d}| j?                  d|       d{   }|r||d<   t        jA                  dt        |      |dz   t        |
             | jC                  |||       d{   }|rd|vr0t        j                  d       tD        |   ||||       d{     yc c}	w 7 7 7 r7 [7 ># 1 d{  7  sw Y   OxY w# t4        $ r%}t        j                  d|dd |       Y d}~d}~ww xY w7 47 7 7 j# t4        $ rM}t        j                  d|dz   t        |
      |d !       tD        |   ||||       d{  7   Y d}~d}~ww xY ww)"a  Send a batch of images as a single Mattermost post with multiple attachments.

        Mattermost supports up to 5 ``file_ids`` per post. Each image is
        uploaded individually (Mattermost's file API is one-at-a-time),
        then a single post is created referencing all uploaded file_ids
        at once. Batches larger than 5 are chunked. Falls back to the
        base per-image loop on total failure.
        Nr   )unquote   zfile://   z%Mattermost: skipping missing image %sz	image/pngr   z-Mattermost: blocked unsafe image URL in batchrU   rV   r   r[   z2Mattermost: failed to download image (HTTP %d): %sr  z&Mattermost: download failed for %s: %sr2   r   r   r   image_r   r   r  rr   z<Mattermost: sending %d image(s) as single post (chunk %d/%d)r   z1Mattermost: multi-image post failed, falling back)human_delayzCMattermost: multi-image send failed (chunk %d/%d), falling back: %sT)exc_info)$r  r   urllib.parser  r	  len	enumerater   r
  append
startswithr   r  r   r   r   r  r  r  r   r>   r*   r_   r`   r  r   r   r  r  r   joinry   r   r   r6   send_multiple_images)rH   r   imagesrw   r  r  r   _unquoteCHUNKir   	chunk_idxr   r  caption_partsr   alt_text
local_pathr  r  r  r   r   re   dl_errfidrm   r   r   erI   s                                 r   r%  z&MattermostAdapter.send_multiple_imagesY  s     4/4QFU/KL!&1u9%LL )& 1 F	fIuQ9q=mmK000"$H')M@f+0 %-'Ix%,,X6 ++I6%-im%<
 , xxz"NN+RT^_$ !&11%8;J{$%LLN	@*95"NN+Z[$%'+}}'8'8 )73H3Hr3H3R (9 ( 
F 
F!%#';;##5$*NN(\(,Ys^%& %-
F 
F 
F 37))+,=	%)%6%6%E+
F 
F !* 0 0a 8 < B B3 G J jPVWZ[cWdVeeiNj $ 1 1'9eR PPC ,K%-N   #*#yy7 (+
 '+&@&@x&P P )6GI&RM9q=#f+ "99'7HUUt4/NN#VW'6wx]h6iiiAF	f M 10
F 
F ->
F 
F 
F 
F  ) %"NN+SU^_b`bUcekl$%
 Q !Q V j fYM3v;D   g27E8Yd2eeefsP  ,OL6O4L$5O>CN1M	4L'
5M	85L3-M	8L*9M	=N?L3L-
L3'M	2L03M	7ANM:NN%O'*NM=AN M?!5NNN
O'M	*M	-L30M	3M9L<:MM			M7M2,N2M77N=N?NN	O<OO	OOOOc                  K   t         }| j                  s	 | j                          d{    t         }| j                  ryddl}|t         z  |j                         z  }t        j"                  ||z          d{    t%        |d
z  t&              }| j                  syy7 w# t        j                  $ r Y yt
        $ r}| j                  rY d}~yddl}t        |      j                         }t        ||j                        r3|j                  dv r%t        j                  d|j                         Y d}~yd|v sd|v sd|v rt        j                  d|       Y d}~yt        j                  d	||       Y d}~Gd}~ww xY w7 	w)zHConnect to the WebSocket and listen for events, reconnecting on failure.Nr   >       u:   Mattermost WS auth failed (HTTP %d) — stopping reconnect401403unauthorizedu8   Mattermost WS permanent error: %s — stopping reconnectu1   Mattermost WS error: %s — reconnecting in %.0fsr  )_RECONNECT_BASE_DELAYrB   _ws_connect_and_listenr   r   r   r   rk   rC   rs   WSServerHandshakeErrorr`   r   r]   r   random_RECONNECT_JITTERr
  min_RECONNECT_MAX_DELAY)rH   delayrg   r   err_strr:  jitters          r   r   zMattermostAdapter._ws_loop  sF    %--`11333-$ }} ..@F--///	#78E; --3 ))  `== c(..*c7#A#ABszzU_G_LL!]_b_i_ijG#u'7>W;TLL![]`aRTWY^__`* 0s   FB# B!
B# AF:F;#FF!B# #F 6F8F  E;FA!E;2F7"E;FE;5F;F  Fc                  K   t        j                  dd| j                        dz   }t        j	                  d|       | j
                  j                  |d       d{   | _        dd	d
| j                  id}| j                  j                  |       d{    t        j	                  d       | j                  2 3 d{   }| j                  r y|j                  |j                  j                  |j                  j                  hv r:	 t        j                  |j                         }| j'                  |       d{    |j                  |j                  j(                  |j                  j*                  |j                  j,                  |j                  j.                  hv st        j	                  d|j                          y7 n7 77 # t        j"                  t$        f$ r Y 3w xY w7 6 yw)z@Single WebSocket session: connect, authenticate, process events.z^httpwsz/api/v4/websocketzMattermost: connecting to %sg      >@)	heartbeatNr   authentication_challenger!   )seqactionr   z1Mattermost: WebSocket connected and authenticatedz!Mattermost: WebSocket closed (%s))r   r   r:   r   r   r>   
ws_connectr?   r;   	send_jsonrB   r   TEXTBINARYrb   loadsr   JSONDecodeError	TypeError_handle_ws_eventERRORCLOSECLOSINGCLOSED)rH   ws_urlauth_msgraw_msgevents        r   r8  z(MattermostAdapter._ws_connect_and_listen  s     $7:MM2F;11&D1II 0dkk*

 hh  ***GH!XX 	 	'}}||!!##   JJw||4E ++E222""""$$##	"  ?N? J 	+	 ,,i8 2 &s   AG8G9G8G%G8=G6GG6AG8G,G8 G4A'G8)#G8G8G6G1-G80G11G86G8c           
       .K   |j                  d      }|dk7  ry|j                  di       }|j                  d      }|sy	 t        j                  |      }|j                  d      | j
                  k(  ry|j                  d      ry|j                  dd	      }| j                  j                  |      ry|j                  d
d	      }|j                  dd      }t        j                  |d      }	|j                  dd	      .|dk7  r&| j                  j                  r%| j                  j                  j                  d      nd}
|
t        j                  dd	      }
t        |
t              rA|
D ch c]5  }t        |      j!                         st        |      j!                         7 }}nFt        |
      j#                  d      D ch c]#  }|j!                         s|j!                         % }}|r||vrt$        j'                  d|       yt        j                  dd      j)                         dv}t        j                  dd	      }|j#                  d      D ch c]#  }|j!                         s|j!                         % }}||v }d| j*                   d| j
                   g}t-        .fd|D              }|r|s|st$        j'                  d|       y|rO|D ]J  }t/        j0                  t/        j2                  |      d	.t.        j4                        j!                         .L |j                  dd	      }|j                  dd	      j7                  d      xs |}|j                  d      xs d}|s| j8                  dk(  r	|dk7  r|r|}|j                  d      xs g }t:        j<                  }.dd  j?                         r/.j7                         jA                  d!      r.j7                         ..jA                  d!      rt:        jB                  }g }g }|D ]  }	 | jE                  d"| d#       d{   }|j                  d$d%|       }tG        |      jH                  xs d	}|j                  d&d'      } d(dl%}!| jL                   d)| }"| jN                  j                  |"d*d+| jP                   i|!jS                  d,-      .      4 d{   }#|#jT                  d/k  r|#jW                          d{   }$d(d0l,m-}%m.}& | jA                  d1      r0 |%|$|xs d2      }'|j_                  |'       |j_                  |        n| jA                  d3      r6d(d4l,m0}(  |(|$|xs d5      }'|j_                  |'       |j_                  |        nM |&|$|      }'|j_                  |'       |j_                  |        n!t$        jc                  d6||#jT                         ddd      d{     |rk|t:        j<                  k(  rXt-        d8 |D              rt:        jf                  }n5t-        d9 |D              rt:        jh                  }n|rt:        jj                  }| jm                  ||	||||:      }*d(d;l,m7}+  |+| j                  j                  |d      },tq        .||*|||r|nd|r|nd|,<      }-| js                  |-       d{    y# t        j                  t        f$ r Y yw xY wc c}w c c}w c c}w 7 7 *7 7 # 1 d{  7  sw Y   -xY w# td        $ r"})t$        jc                  d7||)       Y d})~)'d})~)ww xY w7 w)=z!Process a single WebSocket event.rV  postedNr   rj   user_idr   r   r#   r   channel_typer   r   r   r   allowed_channelsMATTERMOST_ALLOWED_CHANNELS,z7Mattermost: ignoring message in non-allowed channel: %sMATTERMOST_REQUIRE_MENTIONtrue>   0nofalse!MATTERMOST_FREE_RESPONSE_CHANNELS@c              3  ^   K   | ]$  }|j                         j                         v  & y wr}   )rC   )r   patternmessage_texts     r   r   z5MattermostAdapter._handle_ws_event.<locals>.<genexpr>R  s-       <#5#5#77s   *-zAMattermost: skipping non-DM message without @mention (channel=%s))flagssender_namerr   rp   r  r   r2   zfiles/z/infor   file_	mime_typer   r   z/api/v4/files/rN   rK   rU   rV   rX   r[   )cache_image_from_bytescache_document_from_bytesimage/r   audio/)cache_audio_from_bytesz.oggz/Mattermost: failed to download file %s: HTTP %sz)Mattermost: error downloading file %s: %sc              3  >   K   | ]  }|j                  d         yw)rn  Nr#  r   ms     r   r   z5MattermostAdapter._handle_ws_event.<locals>.<genexpr>  s     ?a1<<)?   c              3  >   K   | ]  }|j                  d         yw)ro  Nrr  rs  s     r   r   z5MattermostAdapter._handle_ws_event.<locals>.<genexpr>  s     AQ\\(+Aru  )r   	chat_typerY  	user_namerq   r   )resolve_channel_prompt)ra   message_typesourceraw_messager   
media_urlsmedia_typeschannel_prompt):r*   rb   rK  rL  rM  r<   rG   is_duplicater   r,   r    r'   r(   rs   listrk   r)   r  r   r+   rC   r=   r   r   r   escape
IGNORECASEr^   rD   r   rI  isspacer#  COMMANDrh   r   suffixr   r:   r>   r;   r_   r`   r  gateway.platforms.baserl  rm  r"  rp  r   r   PHOTOVOICEDOCUMENTbuild_sourcery  r   handle_message)/rH   rV  
event_typer   raw_post_strrj   r   r   channel_type_rawrw  allowed_rawcr[  require_mentionfree_channels_rawchfree_channelsis_free_channelmention_patternshas_mentionrf  	sender_idri  rq   r  msg_typer}  r~  r/  	file_infor  extmimer   dl_urlre   r   rl  rm  r-  rp  rg   r{  ry  _channel_prompt	msg_eventrg  s/                                                 @r   rN  z"MattermostAdapter._handle_ws_event  s    YYw'
!yy$xx'	::l+D
 88I$"3"33 88F((4$ ;;##G, XXlB/
88NC8%))*:IF	 xx	2. s" HL{{GXGX$++++//0BC^bK" ii(ErJ+t,<G#Zq3q6<<>CFLLN#Z #Z (+;'7'='=c'B$"#aggiAGGI$  $  J6F$FM  ii,feg12O !#		*Mr R2C2I2I#2N]BRTRZRZR\RXXZ]M](M9O D&&'(D%%&'   / K
 {W  / G#%66		'*BBMM$eg ! HHY+	hh}b188=J HHY'/4	  H, C'I 88J'-2####%,*=*=*?*J*J3*O'..0L""3'"**H !#
!# !	VC V"&--&U0C"DD	!fcUm<5k((.B }}[2LM NN+>#?==,,,}.EF#111; -  l l {{S(*.))+$5	l??84)?	3=RX)YJ&--j9'..t4!__X6U)?	3=RX)YJ&--j9'..t4)B9e)TJ&--j9'..t4'XZ]_c_j_jk-l l!	VH 8{'7'77?;??&,,A[AA&,,&//""! # 
 	B0KKz4
 !!%/zT'2*	
	 !!),,,e $$i0 		L $[$ ^n El %6l l l l.  VJCQTUUVL 	-s  >][ D]1[2[2+]
[7 [72A.] [<6[<F]'\#?\ B\#\\##\\C!\#\#.\
/\#3C]]][/,].[//]\#\#\
\#\ 	\\ 	\##	],]	]	]])r,   r   )returnzDict[str, str])rd   rk   r  Dict[str, Any])rd   rk   rm   r  r  r  )rv   Optional[str]rw   Optional[Dict[str, Any]]r  r  r  bool)r   rk   rm   r  rw   r  r  r  )r   )
r   rk   r   bytesr   rk   r   rk   r  r  )r   r  r  r  r  None)r   rk   r  rk   )NN)
r   rk   r   rk   rv   r  rw   r  r  r   )r   rk   r  r  r}   )r   rk   rw   r  r  r  )
r   rk   r   rk   r   rk   r   r  r  r   )NNN)r   rk   r   rk   r   r  rv   r  rw   r  r  r   )r   rk   r   rk   r   r  rv   r  rw   r  r  r   )NNNN)r   rk   r   rk   r   r  r   r  rv   r  rw   r  r  r   )r   rk   r   rk   r   r  rv   r  rw   r  r  r   )r   rk   r   rk   r   r  rv   r  rw   r  r  r   )r   rk   r  rk   )fileN)r   rk   r$   rk   r   r  rv   r  r  rk   rw   r  r  r   )r   rk   r   rk   r   r  rv   r  r   r  rw   r  r  r   )Ng        )
r   rk   r&  zList[Tuple[str, str]]rw   r  r  floatr  r  )rV  r  r  r  )"__name__
__module____qualname____doc__splits_long_messagesr7   rP   rh   rn   ry   r   r   r   r   r   r   ru   r   r   r   r   r   r   r   r   r   r   r   r   r%  r   r8  rN  __classcell__)rI   s   @r   r0   r0   M   s   @,F
$"0	8;; +; 
	;	";;  ; +	;
 
;8"0	. Uo55*/5;>5NQ5	5: 5:  D00( #'-1<< <  	<
 +< 
<@7 BF

&>
	
 PU??(+?69?HL?	?" "&"&-1

 
 	

  
 +
 

" "&"&-1

 
 	

  
 +
 

" "&#'"&-1

 
 	

 !
  
 +
 

$ "&"&-1

 
 	

  
 +
 

" "&"&-1

 
 	

  
 +
 

( -1>?>? >? 	>?
  >? >? +>? 
>?L $(-1'?'? '? 	'?
  '? !'? +'? 
'?Z .2 _f_f &_f +	_f
 _f 
_fJ 9D%N-r.   r0   F)rq   media_filesforce_documentc          
     t  K   	 ddl }t        | di       xs i j                  d      xs t	        j
                  dd      j                  d	      }t        | d
d      xs t	        j
                  dd      j                         }|r|sddiS d| dd}	dd| i}
|xs g }	 ddlm	}m
}  |d      } ||      \  }} |j                  d/d|j                  d      i|4 d{   }g }|D ]  }t        |t              r|j                  d      n|}|rt        j                  j!                  |      sI|j#                         }|j%                  d|       t'        |d      5 }|j%                  d|j)                         t        j                  j+                  |             ddd        |j,                  | df||
d|4 d{   }|j.                  dvrS|j1                          d{   }dd|j.                   d |dd!  icddd      d{    c cddd      d{    S |j3                          d{   }|j                  d"g       D ](  }|j                  d#      s|j5                  |d#          * ddd      d{     ||d$}|r||d%<   |r||d&<    |j,                  | d'f|	|d(|4 d{   }|j.                  dvrQ|j1                          d{   }dd)|j.                   d |dd!  icddd      d{    cddd      d{    S |j3                          d{   }ddd      d{    d*d+|j                  d#      d,cddd      d{    S # t        $ r ddicY S w xY w7 # 1 sw Y   xY w7 7 7 7 }7 g7 # 1 d{  7  sw Y   xY w7 7 7 7 7 7 # 1 d{  7  sw Y   xY w7 q# 1 d{  7  sw Y   yxY w# |j6                  $ r}dd-| icY d}~S d}~wt8        $ r}dd.| icY d}~S d}~ww xY ww)0u  Send via the Mattermost v4 REST API without a live gateway adapter.

    Used by ``tools/send_message_tool._send_via_adapter`` when the gateway
    runner is not in this process (typical for cron jobs running out-of-process).
    Reads ``MATTERMOST_TOKEN`` from ``pconfig.token`` (set by the gateway
    config loader from env) and falls back to the ``MATTERMOST_TOKEN`` env
    var.  Server URL comes from ``pconfig.extra["url"]`` (set by the YAML
    bridge / env loader) or the ``MATTERMOST_URL`` env var.

    Thread replies (Mattermost CRT) are supported via the ``root_id`` field
    on the ``POST /posts`` payload — pass ``thread_id`` when threading is
    desired.  ``media_files`` are uploaded via ``POST /files``
    (multipart/form-data), then their returned ``file_id`` values are
    attached to the post.

    ``force_document`` is accepted for signature parity with other
    standalone senders but unused — Mattermost stores every uploaded file
    as a generic attachment regardless.
    r   Nr]   z/aiohttp not installed. Run: pip install aiohttpr    r$   r%   r#   r2   r!   r"   zPMattermost standalone send: MATTERMOST_URL and MATTERMOST_TOKEN must both be setrK   rL   rM   rN   )resolve_proxy_urlproxy_kwargs_for_aiohttpMATTERMOST_PROXY)platform_env_varrZ   r   rV   rd   r   rbr   )r   r   )r   rY   >   r\      zMattermost file upload failed (z): r[   r   r   r   rr   r  z/api/v4/postsr   zMattermost API error (T
mattermost)r   platformr   r   z"Mattermost send failed (network): zMattermost send failed: r~   )r   r   r&   r*   r'   r(   r9   r)   r  r  r  r   r_   rs   rt   rd   r  r   r   openr  basenamerj   r`   ra   rb   r"  rc   r   )pconfigr   r   rq   r  r  r   base_urlr!   rY   upload_headersr  r  _proxy_sess_kw_req_kwsessionr  mediar   r   fhupload_resprf   upload_datar   rm   re   r   rg   s                                 r   _standalone_sendr    s    8L
 
'2	&	,"11%8 	+99%r*fSk  Wgt,Q		:Lb0QXXZE54
 	
 #5'**G &'89N#KO; 	W"4FG4V<'(7(( 
)))3

 D	 D	 "$H$ 81;E41HEIIf-e	 y(A'') |W5)T* bNN	!#!1!1)!< #  (7<<j.* 	 8 8
 !"));%0%5%5%77##$$/$6$6#7s4:,!H 8 8 8)D	 D	 D	D )4(8(8(:":K +b A 888D>$OODJ788 8 88F &"'G %.	"&.
##w||*M* 	 ) )
 ;;j0!%,D4T[[M#DSzl,) ) )aD	 D	 D	| "YY[() )   (""hhtn	D	 D	 D	?  LJKKL>D	 8  88)D	D #;8 8 8 88) -)aD	| )) ) ) )aD	 D	 D	 D	J  E=cUCDD ;3C59::;s)  P8M: BP8 AO= $N%O= (A<O($A N$%O(	N

O("N//N 
0N/
O(N#
O(O= )N&*O= .P8/N/N)
*N/.N/O(N,7O(OO("O-O.OO(O	O(O= %O&O= *P8+O>O?OO(OO((O= 4O&5O= 9P8:N
P8	N

P8O= N	O( N/#O(&O= )N/,O(/O5N86O=	O(O	O(O= OO(O#	OO#	O(&O= (O:.O1/O:6O= 9P8:O= =P5PP5P8P5$P0*P5+P80P55P8c                 v   ddl m} m} ddlm}m}m}m}m}  |d        | d      }|r |d        |dd      sy	 |d
        |d        |d       t                 |d      }|r |d|j                  d              |dd      }	|	sy	 |d|	        |d       t                 |d        |d        |d       t                 |d      }
|
r" |d|
j                  dd              |d       n |d       t                 |d        |d        |d        |d       }|r	 |d!|        |d"       y	)#a  Guide the user through Mattermost bot setup.

    Mirrors Discord/Teams' ``interactive_setup`` shape: lazy-imports CLI
    helpers so the plugin's import surface stays small, prompts for the
    server URL + bot token, captures an allowlist, and offers to set a
    home channel.  Replaces the central
    ``hermes_cli/setup.py::_setup_mattermost`` function this migration
    removes.
    r   )get_env_valuesave_env_value)promptprompt_yes_noprint_header
print_infoprint_success
Mattermostr"   zMattermost: already configuredzReconfigure Mattermost?FNz/Works with any self-hosted Mattermost instance.uF      1. In Mattermost: Integrations → Bot Accounts → Add Bot Accountz   2. Copy the bot tokenz3Mattermost server URL (e.g. https://mm.example.com)r%   r2   z	Bot tokenT)passwordzMattermost token savedu,   🔒 Security: Restrict who can use your botu6      To find your user ID: click your avatar → Profilez'   or use the API: GET /api/v4/users/mez?Allowed user IDs (comma-separated, leave empty for open access)MATTERMOST_ALLOWED_USERS r#   zMattermost allowlist configureduE   ⚠️  No allowlist set - anyone who can message the bot can use it!uL   📬 Home Channel: where Hermes delivers cron job results and notifications.uH      To get a channel ID: click channel name → View Info → copy the IDzK   You can also set this later by typing /set-home in a Mattermost channel.z9Home channel ID (leave empty to set later with /set-home)MATTERMOST_HOME_CHANNELz2   Open config in your editor:  hermes config edit)hermes_cli.configr  r  hermes_cli.cli_outputr  r  r  r  r  printr9   replace)r  r  r  r  r  r  r  existingmm_urlr!   allowed_usershome_channels               r   interactive_setupr  ]  sH    @  /0H346>@AWX)*	GIJF's);<;.E%u-*+	G=>GH89	G\]M1=3H3Hb3QR78Z[	G]^YZ\]UVL0,?CDr.   c                @   d|v rBt        j                  d      s-t        |d         j                         t         j                  d<   |j                  d      }|Yt        j                  d      sDt        |t              rdj                  d |D              }t        |      t         j                  d<   |j                  d      }|Yt        j                  d	      sDt        |t              rdj                  d
 |D              }t        |      t         j                  d	<   y)u  Translate ``config.yaml`` ``mattermost:`` keys into env vars.

    Implements the ``apply_yaml_config_fn`` contract (#24836 / #25443).
    Mirrors the legacy ``mattermost_cfg`` block that used to live in
    ``gateway/config.py::load_gateway_config()`` before this migration.

    The MattermostAdapter reads its runtime configuration via
    ``os.getenv()`` for ``MATTERMOST_REQUIRE_MENTION``,
    ``MATTERMOST_FREE_RESPONSE_CHANNELS``, and
    ``MATTERMOST_ALLOWED_CHANNELS``.  Rather than rewrite those call sites
    to read from ``PlatformConfig.extra``, this hook keeps the env-driven
    model and merely owns the YAML→env translation here, next to the
    adapter that consumes it.

    Env vars take precedence over YAML — every assignment is guarded
    by ``not os.getenv(...)`` so an explicit env var survives a config.yaml
    update.  Returns ``None`` because no extras are seeded into
    ``PlatformConfig.extra`` directly (everything flows through env).
    r  r^  free_response_channelsNrc  r]  c              3  2   K   | ]  }t        |        y wr}   rk   r   vs     r   r   z%_apply_yaml_config.<locals>.<genexpr>  s     /a3q6/   r[  r\  c              3  2   K   | ]  }t        |        y wr}   r  r  s     r   r   z%_apply_yaml_config.<locals>.<genexpr>  s     -Q#a&-r  )	r'   r(   rk   rC   environr*   rs   r  r$  )yaml_cfgmattermost_cfgfrcacs       r   _apply_yaml_configr    s    ( N*299=Y3Z36~FW7X3Y3_3_3a

/0


5
6C
ryy)LMc4 ((/3//C:=c(

67			.	/B	~bii(EFb$-"--B47G

01r.   c                    ddl m} t        |j                  d      xs dj	                         xr# |j                  d      xs dj	                               S )ay  Mattermost is considered connected when BOTH MATTERMOST_TOKEN and
    MATTERMOST_URL are set.

    Looks up via ``hermes_cli.gateway.get_env_value`` at call time (not via
    the plugin's own bound import) so tests that patch
    ``gateway_mod.get_env_value`` can suppress ambient env vars.  Matches
    what the legacy connected-platforms check did before this migration.
    r   Nr"   r#   r%   )hermes_cli.gatewaygatewayr  r  r)   )r,   gateway_mods     r   _is_connectedr    sT     -		"	"#5	6	<"CCE 	H&&'78>BEEG r.   c                    t        |       S )zHFactory wrapper that constructs MattermostAdapter from a PlatformConfig.)r0   )r,   s    r   _build_adapterr    s    V$$r.   c                    | j                  ddt        t        t        t        ddgdt
        t        dddt        t        d	d
       y)u:   Plugin entry point — called by the Hermes plugin system.r  r  r%   r"   zpip install aiohttpr  MATTERMOST_ALLOW_ALL_USERSr  u   💬T)r   labeladapter_factorycheck_fnvalidate_configis_connectedrequired_envinstall_hintsetup_fnapply_yaml_config_fnallowed_users_envallow_all_envcron_deliver_env_varstandalone_sender_fnmax_message_lengthemojiallow_update_commandN)	register_platformr  r   r-   r  r  r  r  r   )ctxs    r   registerr
    sW    &.2"&(:;* # 0426
 . +!G  $r.   r  )r,   r   r  r  )r   rk   r   rk   rq   r  r  zOptional[list]r  r  r  r  r  )r  rt   r  rt   r  zdict | None)+r  
__future__r   r   rb   loggingr'   r   pathlibr   typingr   r   r   r   r	   gateway.configr
   r   gateway.platforms.helpersr   r  r   r   r   r   	getLoggerr  r   r   r   r7  r=  r;  r   r-   r0   r  r  r  r  r  r
  r~   r.   r   <module>r     s   #    	 	  3 3 3 9  
		8	$  
				     y-+ y-P  $"& E;E; E;
 E;  E; E; E;Z:ED!R*%
&r.   