
    Oj,                    (   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
  e
e      j                         j                  j                  ZdZg dZedz  ZdadaddZdd	Zdd
Zd ZddZddZdddZddZddZddZd dZd!dZd"dZ d Z!y)#u  Microsoft Teams (send/read chats) in-process MCP for Jarvis.

Gives Claude real Teams hands — message people, list recent chats, read a
conversation — as Ahmed himself, via Microsoft Graph with DELEGATED
permissions (acts on his behalf, in his name). No bot, no metered API
(Teams Graph APIs stopped being metered in Aug 2025).

Auth: MSAL device-code flow against an Azure Entra app registration Ahmed
(as tenant admin) creates once. Public client, NO client secret. The first
Teams action prints a short code + URL (microsoft.com/devicelogin); enter it
once and the token caches to ``teams_token_cache.json`` and refreshes
silently forever after.

Runs IN-PROCESS via the Agent SDK MCP server (key ``"teams"``; Claude sees
``mcp__teams__<name>``). ``msal`` is imported lazily so importing this module
never hard-fails when it's missing.

Azure setup (one-time, Ahmed as admin — see README):
  1. Entra ID → App registrations → New. Note the Application (client) ID and
     Directory (tenant) ID.
  2. Authentication → Allow public client flows = Yes (enables device code).
  3. API permissions → Microsoft Graph → Delegated: Chat.ReadWrite,
     ChatMessage.Send, User.ReadBasic.All, User.Read → Grant admin consent.

Config (``.env`` / env):
  TEAMS_CLIENT_ID   the Application (client) ID  (REQUIRED — dark without it)
  TEAMS_TENANT_ID   Directory (tenant) ID, or "organizations" (default)
Set ``MCP_TEAMS=0`` to disable.
    )annotationsN)Pathz https://graph.microsoft.com/v1.0)zChat.ReadWritezChatMessage.SendzUser.ReadBasic.Allz	User.Readzteams_token_cache.jsonc                     t         j                  j                  dd      dk(  ryt        t         j                  j                  dd      j	                               S )zjWhether the Teams MCP should be exposed to Claude. Dark until the
    Azure app's client id is configured.	MCP_TEAMS10FTEAMS_CLIENT_ID )osenvirongetboolstrip     A/Users/ahmed/devFolder/Ultron/claude-voice/voice/teams_control.pyenabledr   2   sB     
zz~~k3'3.

0"5;;=>>r   c                    dd| dgiS )Ncontenttext)typer   r   )msgs    r   _textr   :   s    5677r   c                t    dd l }|j                  dd| xs d      j                  dd      j                         S )Nr   z<[^>]+>r
   z&nbsp; )resubreplacer   )sr   s     r   _strip_htmlr    >   s2    66*b!'r*228SAGGIIr   c                    t         t         S dd l} | j                         }t        j	                         r#|j                  t        j                                t        j                  j                  dd      j                         xs d}| j                  t        j                  d   j                         d| |      a |t         _        t         S )Nr   TEAMS_TENANT_IDorganizationsr	   z"https://login.microsoftonline.com/)	authoritytoken_cache)_APPmsalSerializableTokenCache_CACHEexistsdeserialize	read_textr   r   r   r   PublicClientApplication	_cv_cache)r'   cachetenants      r   	_msal_appr1   G   s    '')E}}&**,-ZZ^^-?EEG  ''


$%++-6vh? ( D
 DNKr   c                    t        | dd       }|1|j                  r$t        j                  |j	                                y y y )Nr.   )getattrhas_state_changedr)   
write_text	serialize)appr/   s     r   _persistr8   Z   s;    Cd+EU44%//+, 5r   c                    t               } d}| j                         }|r| j                  t        |d         }|s]| j	                  t              }d|vrt        d|j                  d|             t        d|d	   z   d
z          | j                  |      }t        |        d|vrt        d|j                  d|             |d   S )zwReturn a valid Graph access token, running the one-time device-code
    sign-in if there's no cached account. Blocking.Nr   )account)scopes	user_codezdevice flow failed: error_descriptionz&
  [teams] ONE-TIME SIGN-IN NEEDED:
  message
access_tokenztoken error: )
r1   get_accountsacquire_token_silent_SCOPESinitiate_device_flowRuntimeErrorr   printacquire_token_by_device_flowr8   )r7   resultaccountsflows       r   _tokenrK   `   s     +CF!H))'8A;)G''w'7d"&txx0CT'J&KLN N 	84	?J 	11$7SMV#FJJ':FCDEG 	G.!!r   c                   |j                  d      r|nt        |z   }|#t        j                  |      j	                         nd }t
        j                  j                  |||       }|j                  ddt                       |j                  dd       	 t
        j                  j                  |d      5 }|j                         }|rt        j                  |      ni cd d d        S # 1 sw Y   y xY w# t
        j                  j                  $ rI}|j                         j                  d	d
      d d }	t!        d|  d| d|j"                   d|	       d }~ww xY w)Nhttp)datamethodAuthorizationzBearer zContent-Typezapplication/json   )timeoutzutf-8r   i  zGraph r   z -> : )
startswith_GRAPHjsondumpsencodeurllibrequestRequest
add_headerrK   urlopenreadloadserror	HTTPErrordecoderE   code)
rO   pathbodyurlrN   reqrrawedetails
             r   _graphrl   |   s   //&)$v}C(,(84::d""$dD
..
 
 4
 
?CNN?gfhZ$89NN>#56K^^##C#4&&(C&)4::c?r 544 <<!! K)4Tc:VF81TF$qvvhbIJJKs7   !C6 7)C* 	C6 *C3/C6 3C6 6EAEEc                 z    t         0t        dd      } | j                  d      | j                  dd      da t         S )NGETz/meiddisplayNameme)ro   rp   )_MErl   r   )ms    r   _mert      s5    
{5% UU4[}d1KLJr   c                   | j                         } d| v rY	 t        ddt        j                  j	                  |              }|d   |j                  d|       |j                  d      xs | dS t        j                  j	                  d|  d	      }t        dd
| d      }|j                  dg       }|sy|d   }|d   |j                  d|       |j                  d      dS # t        $ r Y w xY w)zEResolve a person by display name or email to {id, displayName, mail}.@rn   z/users/ro   rp   mail)ro   rp   rw   zstartswith(displayName,'')z/users?$filter=z#&$top=5&$select=id,displayName,mailvalueNr   )r   rl   rY   parsequoter   	Exception)nameuqresuserss        r   
_find_userr      s   ::<D
d{	u(:(:4(@'ABCAD'!%%t2LEE&M1T3 3 	5dV2>?A
/! -7 7 8CGGGR EaAD'!%%t*DEE&M# #  		s   AC# #	C/.C/c                n    t               }dddgd|d    ddddgd|  ddgd}t        d	d
|      d   S )u=   Get (or create — Graph dedups) the 1:1 chat id with a user.oneOnOnez*#microsoft.graph.aadUserConversationMemberownerz(https://graph.microsoft.com/v1.0/users('ro   rx   )z@odata.typeroleszuser@odata.bind)chatTypemembersPOSTz/chats)rt   rl   )user_idrq   re   s      r   _one_on_one_chatr      si    	BHi;BtH:RHJ Ii;G9BGI	
D &(D)$//r   c                    t        |       }|sd|  dS t        |d         }t        dd| ddd|ii       d	|d
    dS )NCouldn't find anyone named 'z' in your organization.ro   r   /chats/z	/messagesre   r   z	Messaged rp   z
 on Teams.)r   r   rl   )personr>   r~   chat_ids       r   
_send_chatr      sd    6A-fX5LMMqw'G
6WWIY/Y()+q'(
33r   c                r   t        dd|  d      }|j                  dg       }|syt               d   }g }|D ]  }|j                  d      dk(  rY|j                  d	g       D cg c](  }|j                  d
      |k7  r|j                  dd      * }}dj                  |      xs d}n|j                  d      xs d}t	        |j                  d      xs i j                  di       j                  dd            d d }	|j                  d|d    d| |	rd|	 ndz           dj                  |      S c c}w )Nrn   z/me/chats?$top=zA&$expand=members&$orderby=lastMessagePreview/createdDateTime descry   zNo recent Teams chats.ro   r   r   r   userIdrp   ?z, z	(unknown)topicz(group chat)lastMessagePreviewre   r   r
   P   [z] u    — r?   )rl   r   rt   joinr    append)
limitr   chatsmy_idlinescrs   othersr}   prevs
             r   _list_chatsr      sQ   
/% 1L L MCGGGR E'E$KEE55
*9:y"9M 39MAxE1 eeM3/9MF 399V$3D55>3^DUU'(.B33FB?CC2  #% 	q4	D6*dVn"MN  99U3s   "-D4c                   t        |       sd|  dS t        dd      }t               d   }d}|j                  dg       D ]B  }|j                  d      d	k7  rt	        fd
|j                  dg       D              s=|d   } n |s	dd    dS t        dd| d|       j                  dg       }|s	dd    dS g }t        |      D ]~  }|j                  d      xs i j                  di       j                  d      xs d}	t        |j                  di       j                  dd            }
|
si|j                  |	 d|
         dd    ddj                  |      z   S )z=Read the last `count` messages of the 1:1 chat with a person.r   z'.rn   z!/me/chats?$top=50&$expand=membersro   Nry   r   r   c              3  L   K   | ]  }|j                  d       d   k(    yw)r   ro   N)r   ).0rs   r~   s     r   	<genexpr>z_read_chat.<locals>.<genexpr>   s%     H3GaquuX!D')3Gs   !$r   zNo existing Teams chat with rp   .r   z/messages?$top=zNo messages in the chat with fromusersystemre   r   r
   rS   z
Chat with z:
r?   )	r   rl   rt   r   anyreversedr    r   r   )r   countr   r   r   r   msgsr   rs   whor   r~   s              @r   
_read_chatr      s   6A-fXR88
;
<CE$KEGWWWb!55
*H155B3GHHgG " -a.>-?qAA%77)?5'B #gr* 	.q/?.@BBEd^uuV}"''377 '& 	155,00B?@LLC54&)*  -()-		%0@@@r   c                     ddl ddlm} m}  | ddt        t        d      dfd       } | dd	d
t
        i      dfd       } | ddt        t
        d      dfd       } |dd|||g      S )z5Build and return the in-process ``teams`` MCP server.r   N)toolcreate_sdk_mcp_server
teams_sendzSend a Microsoft Teams message to a person by name or email. Finds them in Ahmed's organization and messages their 1:1 chat, as Ahmed.)r   r>   c                l  K   t        | j                  dd            j                         }t        | j                  dd            j                         }|r|st        d      S 	 t        j	                  t
        ||       d {         S 7 # t        $ r}t        d|       cY d }~S d }~ww xY ww)Nr   r
   r>   z0teams_send failed: need both person and message.zteams_send failed: )strr   r   r   	to_threadr   r|   )argsr   r>   rj   asyncios       r   r   z build_server.<locals>.teams_send   s     
 TXXh+,224dhhy"-.446WKLL	4w00VWMMNNM 	4.qc233	4sH   A"B4&B BB B4B 	B1B,&B1'B4,B11B4teams_list_chatszfList Ahmed's most recent Teams chats (people/groups) with a chat id and last-message preview for each.r   c           
       K   	 t        dt        t        | j                  d      xs d      d            }t	        j                  t        |       d {         S 7 # t        $ r}t	        d|       cY d }~S d }~ww xY ww)N   r      rQ   zteams_list_chats failed: )maxminintr   r   r   r   r|   )r   r   rj   r   s      r   r   z&build_server.<locals>.teams_list_chats  sr     	:3s488G#4#:;R@AEw00eDDEED 	:4QC899	:sG   BAA  AA  BA   	B)A<6B7B<BBteams_read_chatzTRead the recent messages of Ahmed's 1:1 Teams chat with a person (by name or email).)r   r   c           
     z  K   t        | j                  dd            j                         }|st        d      S 	 t	        dt        t        | j                  d      xs d      d            }t        j                  t        ||       d {         S 7 # t        $ r}t        d|       cY d }~S d }~ww xY ww)	Nr   r
   z"teams_read_chat failed: no person.r   r   
   rQ   zteams_read_chat failed: )
r   r   r   r   r   r   r   r   r   r|   )r   r   r   rj   r   s       r   r   z%build_server.<locals>.teams_read_chat  s      TXXh+,224=>>	93s488G#4#:;R@AEw00VUKKLLK 	93A3788	9sG   7B;AB BB B;B 	B8 B3-B8.B;3B88B;teamsz1.0.0)r}   versiontools)r   dictreturnr   )r   claude_agent_sdkr   r   r   r   )r   r   r   r   r   r   s        @r   build_serverr      s    <	,PS
)+4	+4 

/18#@:@: 

 ,/#!>@9@9 !+_= r   )r   r   )r   r   r   r   )r   r   r   r   )r   None)r   r   )N)rO   r   rd   r   re   dict | Noner   r   )r   r   )r}   r   r   r   )r   r   r   r   )r   r   r>   r   r   r   )r   r   r   r   )r   r   r   r   r   r   )"__doc__
__future__r   rV   r   urllib.errorrY   urllib.parseurllib.requestpathlibr   __file__resolveparent_ROOTrU   rC   r)   r&   rr   r   r   r    r1   r8   rK   rl   rt   r   r   r   r   r   r   r   r   r   <module>r      s   < #  	    X ''..	+	)	)
?8J&-"8K#*0&4,A>/r   