
    `gj\                    >   U 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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  ej&                  d      Z	 ddlmZmZ dd
lmZ d.dZ ej:                  d      Zd0dZd.dZ d.dZ!d.dZ"d.dZ#d1d2dZ$d.dZ%d1d2dZ&dgZ'de(d<   g dZ)dZ*d1d3dZ+d1d4dZ,d5dZ-d6dZ.d7dZ/d8dZ0d9d Z1d:d!Z2d;d"Z3d1d<d#Z4d=d$Z5dd%	 	 	 d>d&Z6d1d?d'Z7d@d(Z8d1dAd)Z9d1dBd*Z:d1dAd+Z;d:d,Z<e=d-k(  r e<        yy# eef$ r d.dZd/d	ZY w xY w)Cu
  User OAuth helper for the Google Chat gateway adapter.

Google Chat's ``media.upload`` REST endpoint hard-rejects service-account
authentication:

    "This method doesn't support app authentication with a service
     account. Authenticate with a user account."

(See https://developers.google.com/workspace/chat/api/reference/rest/v1/media/upload
and https://developers.google.com/chat/api/guides/auth/users.)

For the bot to deliver native file attachments — the same drag-and-drop
file widget the user gets when they upload manually — each user must
grant the bot the ``chat.messages.create`` scope ONCE in their own DM.
The bot stores per-user refresh tokens and calls ``media.upload`` plus
the subsequent ``messages.create`` *as the requesting user* whenever a
file needs sending.

This module is BOTH a CLI tool (driven by the agent via slash commands or
terminal commands) AND a library imported by ``google_chat.py``:

    Library functions (called from the adapter at runtime):
        load_user_credentials(email=None) -> Credentials | None
        refresh_or_none(creds, email=None) -> Credentials | None
        build_user_chat_service(creds) -> chat_v1.Resource
        list_authorized_emails() -> List[str]

    CLI commands (driven by the agent through the /setup-files slash
    command, modeled on skills/productivity/google-workspace/scripts/setup.py):
        --check                          Exit 0 if auth is valid, else 1
        --client-secret /path/to.json    Persist OAuth client credentials
        --auth-url                       Print the OAuth URL for the user
        --auth-code CODE                 Exchange auth code for token
        --revoke                         Revoke and delete stored token
        --install-deps                   Install Python dependencies
        --email EMAIL                    Scope CLI ops to a specific user
                                         (defaults to legacy single-user
                                         mode when omitted)

The flow mirrors the existing google-workspace skill exactly so anyone
familiar with that flow can read this without surprises.

Token storage layout
--------------------
- Per-user tokens (keyed by sender email):
    ``${HERMES_HOME}/google_chat_user_tokens/<sanitized_email>.json``
- Legacy single-user token (fallback, untouched for backward compat):
    ``${HERMES_HOME}/google_chat_user_token.json``
- Per-user pending OAuth state during /setup-files start → exchange:
    ``${HERMES_HOME}/google_chat_user_oauth_pending/<sanitized_email>.json``
- Legacy pending state:
    ``${HERMES_HOME}/google_chat_user_oauth_pending.json``
- OAuth client secret (profile-scoped — each profile registers its own):
    ``${HERMES_HOME}/google_chat_user_client_secret.json``
    )annotationsN)Path)AnyListOptionalTuplez(gateway.platforms.google_chat_user_oauth)display_hermes_homeget_hermes_homec                     t         j                  j                  dd      j                         } | rt	        |       S t	        j
                         dz  S )NHERMES_HOME z.hermes)osenvirongetstripr   home)vals    V/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/google_chat/oauth.pyr
   r
   S   s;    jjnn]B/557tCy<TYY[9%<<    c                     t               } 	 dt        | j                  t        j                                     z   S # t
        $ r t        |       cY S w xY w)Nz~/)r
   strrelative_tor   r   
ValueError)r   s    r   r	   r	   W   sI     	#d..tyy{;<<< 	t9	s   .; AA)atomic_replacec                     t               S )a  Resolve HERMES_HOME at call time (NOT module import).

    Tests and ``HERMES_HOME=...`` env overrides need this to be late-
    binding. If we cached the path at import time, switching profiles
    or tweaking env vars in tests would silently keep using the old
    path.)r
    r   r   _hermes_homer   a   s     r   z[^a-z0-9._@-]+c                z    t         j                  d| xs dj                         j                               }|xs dS )N_r   	_unknown_)_EMAIL_FS_REsubr   lower)emailcleaneds     r   _sanitize_emailr&   r   s5    sU[b$7$7$9$?$?$ABG!k!r   c                     t               dz  S )Nzgoogle_chat_user_token.jsonr   r   r   r   _legacy_token_pathr)   w   s    >999r   c                     t               dz  S )Ngoogle_chat_user_tokensr(   r   r   r   _user_tokens_dirr,   {   s    >555r   c                     t               dz  S )Nz#google_chat_user_oauth_pending.jsonr(   r   r   r   _legacy_pending_pathr.          >AAAr   c                     t               dz  S )Ngoogle_chat_user_oauth_pendingr(   r   r   r   _user_pending_dirr2      s    ><<<r   c                L    | rt               t        |        dz  S t               S )z?Return the on-disk token path for ``email`` or the legacy path..json)r,   r&   r)   r$   s    r   _token_pathr6      s)    !u'=&>e$DDDr   c                     t               dz  S )Nz#google_chat_user_client_secret.jsonr(   r   r   r   _client_secret_pathr8      r/   r   c                L    | rt               t        |        dz  S t               S )Nr4   )r2   r&   r.   r5   s    r   _pending_auth_pathr:      s)     "(>'?u%EEE!!r   z4https://www.googleapis.com/auth/chat.messages.create	List[str]SCOPES)zgoogle-api-python-clientzgoogle-auth-oauthlibzgoogle-auth-httplib2zhttp://localhost:1c                   t        |       }|j                         sy	 ddlm} ddlm} 	 |j                  t        |            }|j                  r|S |j                  r1|j                  r%	 |j                   |              t!        ||       |S y# t        $ r t        j                  d       Y yw xY w# t        $ r!}t        j                  d||       Y d}~yd}~ww xY w# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)uK  Load + validate persisted user OAuth credentials.

    ``email`` selects the per-user token file; ``None`` falls back to the
    legacy single-user path (left in place for installs that ran the
    pre-multi-user flow). Returns a ``google.oauth2.credentials.Credentials``
    instance ready for use, or ``None`` if no token is stored, the token
    is corrupt, or refresh fails. Adapter callers should treat ``None``
    as "user has not run /setup-files yet" and surface the setup-instructions
    fallback to the user.

    Does NOT raise on the no-token case — that's expected.
    Nr   CredentialsRequestz[google_chat_user_oauth] google-auth not installed; user-OAuth attachment delivery is disabled. Install hermes-agent[google_chat].z3[google_chat_user_oauth] token at %s is corrupt: %szS[google_chat_user_oauth] token refresh failed (user should re-run /setup-files): %s)r6   existsgoogle.oauth2.credentialsr?   google.auth.transport.requestsrA   ImportErrorloggerwarningfrom_authorized_user_filer   	Exceptionvalidexpiredrefresh_tokenrefresh_persist_credentials)r$   
token_pathr?   rA   credsexcs         r   load_user_credentialsrR      s    U#J9:
 55c*oF {{}},,	MM')$ 	UJ/ K  R	
   A	
   	NN236 	s@   B B5 ,C" B21B25	C>CC"	D+DDc                6   | y| j                   r| S 	 ddlm} | j                  r:| j
                  r.	 | j                   |              t        | t        |             | S y# t        $ r Y yw xY w# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)uh  Refresh ``creds`` if expired. Returns the credentials or ``None``.

    Used by the adapter just before calling media.upload to ensure the
    token is current. Returns ``None`` if refresh fails — caller falls
    back to the text-notice path. ``email`` controls where the refreshed
    token is written back; ``None`` keeps the legacy single-file path.
    Nr   r@   z+[google_chat_user_oauth] refresh failed: %s)rJ   rD   rA   rE   rK   rL   rM   rN   r6   rI   rF   rG   )rP   r$   rA   rQ   s       r   refresh_or_nonerT      s     }{{: }},,	MM')$ E(:;L     	NN=s 		s(   A  ,A/  	A,+A,/	B8BBc                &    ddl m}  |dd| d      S )a	  Build a Google Chat API client authenticated as the user.

    Used for media.upload + the subsequent messages.create that
    references the attachmentDataRef. The bot's separate SA-authed
    client (``self._chat_api`` in the adapter) is for everything else.
    r   )buildchatv1F)credentialscache_discovery)googleapiclient.discoveryrV   )rP   build_services     r   build_user_chat_servicer]     s     A5%PPr   c                    t               } | j                         sg S g }| j                         D ]>  }|j                         s|j                  dk(  s$|j                  |j                         @ |j                          |S )u]  Return the set of user emails that have stored per-user tokens.

    Lists files in the per-user tokens dir; does NOT include the legacy
    single-user token (its owner is unknown). Sanitized filenames lose
    the ``+suffix`` part of plus-addressed emails — accept that and use
    this list only for admin display, not for trust decisions.
    r4   )r,   rB   iterdiris_filesuffixappendstemsort)doutfs      r   list_authorized_emailsrh     sh     	A88:	CYY[ 99;188w.JJqvv HHJJr   c           	         	 t        |t        t        j                  | j	                                            y# t
        $ r t        j                  d|d       Y yw xY w)zBPersist refreshed credentials atomically with private permissions.z<[google_chat_user_oauth] failed to persist credentials at %sT)exc_infoN)_write_private_json"_normalize_authorized_user_payloadjsonloadsto_jsonrI   rF   debug)rP   rO   s     r   rN   rN   .  sS    	
.tzz%--//JK	
  
J 	 	

s   69 !AAc                H    t        |       }|j                  d      sd|d<   |S )zGEnsure the persisted token JSON has the type field google-auth expects.typeauthorized_user)dictr   )payload
normalizeds     r   rl   rl   A  s'    gJ>>&!.
6r   c                Z   | j                   j                  dd       	 t        j                  | j                   d       | j                  dt        j                          dt        j                  d             }	 t        j                  t        |      t        j                  t        j                  z  t        j                  z  t        j                  t        j                   z        }t        j"                  |dd	      5 }t%        j&                  ||d
d       |j)                          t        j*                  |j-                                ddd       t/        ||        	 t        j                  | t        j                  t        j                   z         	 |j1                         r|j3                          yy# t        $ r Y w xY w# 1 sw Y   ~xY w# t        $ r Y Jw xY w# t        $ r Y yw xY w# 	 |j1                         r|j3                          w w # t        $ r Y w w xY wxY w)z=Atomically write JSON with 0o600 permissions where supported.T)parentsexist_oki  z.tmp..   wzutf-8)encoding   F)indentensure_asciiN)parentmkdirr   chmodOSErrorwith_suffixgetpidsecrets	token_hexopenr   O_WRONLYO_CREATO_EXCLstatS_IRUSRS_IWUSRfdopenrm   dumpflushfsyncfilenor   rB   unlink)pathdatatmp_pathfdfhs        r   rk   rk   I  s   KKdT2
e$ %		}Ag6G6G6J5K LMHWWMKK"**$ryy0LL4<<'

 YYr31 	"RIIdBqu=HHJHHRYY[!	" 	x&	HHT4<<$,,67	 ! !+  	" 	"  		  			 ! ! 		s    F= <BG7 AGG7 %5G  G( =	G
	G
GG7 	G%"G7 $G%%G7 (	G43G47H*9 HH*	H'$H*&H''H*c                 v    	 ddl } ddl}y# t        $ r$ t               st	        j
                  d       Y yY yw xY w)z6Check deps available; install if not; exit on failure.r   N   )googleapiclientgoogle_auth_oauthlibrE   install_depssysexit)r   r   s     r   _ensure_depsr   i  s1    # ~HHQK s    (88c                    	 dd l } dd l}t        d       y# t        $ r Y nw xY wt        d       	 ddlm}  |dgt        z         }|j                  dk7  r*t        |j                  xs dj                         d d       t        d	       y# t        $ r.}t        d
|        t        d       t        d       Y d }~yd }~ww xY w)Nr   zDependencies already installed.Tz,Installing Google Chat OAuth dependencies...)_pip_installz--quietzinstall failedi,  zDependencies installed.z'ERROR: Failed to install dependencies: z"Or install via the optional extra:z)  pip install 'hermes-agent[google_chat]'F)r   r   printrE   hermes_cli.tools_configr   _REQUIRED_PACKAGES
returncodeRuntimeErrorstderrr   rI   )r   r   r   resultrQ   s        r   r   r   s  s    #/0  

898yk,>>?! A1AHHJ4CPQQ'( 7u=>239:	s$    	""AB 	C$B>>Cc                    t        |       }|j                         st        d|        yt        |       }|t        d| d       yt        d|        y)zxPrint status; return True if creds are usable.

    Per-user when ``email`` given, legacy single-user when omitted.
    zNOT_AUTHENTICATED: No token at Fz*TOKEN_INVALID: Re-run /setup-files (path: )zAUTHENTICATED: Token valid at T)r6   rB   r   rR   )r$   rO   rP   s      r   
check_authr     sd    
 U#J/
|<=!%(E}::,aHI	*:,
78r   c                    t        |       j                         j                         }|j                         s#t	        d|        t        j                  d       	 t        j                  |j                               }dvr/d|vr+t	        d       t	        d       t        j                  d       t               }t        ||       t	        d|        y	# t        j                  $ r# t	        d       t        j                  d       Y w xY w)
zGValidate and copy the user's OAuth client_secret.json into HERMES_HOME.zERROR: File not found: r   zERROR: File is not valid JSON.	installedwebzPERROR: Not a Google OAuth client secret file (missing 'installed' or 'web' key).z@Download from: https://console.cloud.google.com/apis/credentialszOK: Client secret saved to N)r   
expanduserresolverB   r   r   r   rm   rn   	read_textJSONDecodeErrorr8   rk   )r   srcr   targets       r   store_client_secretr     s    
t*


!
)
)
+C::<'u-.zz#--/*
 $5#4)	
 	N	
 	 "F%	'x
01!  ./s   #C 3DDr5   c                L    t        |      }t        || |t        |xs dd       y )Nr   )statecode_verifierredirect_urir$   )r:   rk   _REDIRECT_URI)r   r   r$   pendings       r   _save_pending_authr     s,     'G*)[b		
r   c                   t        |       }|j                         s t        d       t        j                  d       	 t        j                  |j                               }j                  d      r|j                  d      s+t        d       t        d       t        j                  d       |S # t        $ r8}t        d|        t        d       t        j                  d       Y d }~d }~ww xY w)	Nz<ERROR: No pending OAuth session found. Run --auth-url first.r   z-ERROR: Could not read pending OAuth session: z.Run --auth-url again to start a fresh session.r   r   z2ERROR: Pending OAuth session is missing PKCE data.zRun --auth-url again.)
r:   rB   r   r   r   rm   rn   r   rI   r   )r$   r   r   rQ   s       r   _load_pending_authr     s     'G>>LMzz'++-.
 88GDHH_$=BC%&K  =cUCD>?s   #B/ /	C08.C++C0c                    | j                  d      s| dfS ddlm}m}  ||       } ||j                        }d|vr t        d       t        j                  d       |j                  ddg      d   }|d   d   |fS )	zGAccept a raw auth code OR the full failed-redirect URL the user pastes.httpNr   parse_qsurlparsecodez(ERROR: No 'code' parameter found in URL.r   r   )	
startswithurllib.parser   r   queryr   r   r   r   )code_or_urlr   r   parsedparamsr   s         r   _extract_code_and_stater     s}    !!&)D  /k"Ffll#FV89JJw'*E&>!e##r   c                b   t               j                         s t        d       t        j                  d       t                ddlm} |j                  t        t                     t        t        d      }|j                  dd	      \  }}t        ||j                  | 
       t        |       y)zPrint the OAuth URL for the user to visit. Persists PKCE state.

    ``email`` namespaces the pending state so two users can be mid-flow
    in parallel without trampling each other's PKCE verifier.
    :ERROR: No client secret stored. Run --client-secret first.r   r   FlowT)scopesr   autogenerate_code_verifierofflineconsent)access_typeprompt)r   r   r$   N)r8   rB   r   r   r   r   google_auth_oauthlib.flowr   from_client_secrets_filer   r<   r   authorization_urlr   r   )r$   r   flowauth_urlr   s        r   get_auth_urlr     s      '')JKN.((!""#'	 ) D ,, - OHe U$2D2DER	(Or   c                v   t               j                         s t        d       t        j                  d       t        |      }| }t        |       \  } }|r(||d   k7  r t        d       t        j                  d       t                ddlm	} ddl
m}m} t        t              }t        |t               rb|j#                  d      rQ | ||      j$                        }	|	j'                  d	      xs d
gd   j)                         }
|
r|
j+                         }|j-                  t!        t                     ||j'                  dt.              |d   |d         }	 dt0        j2                  d<   |j5                  |        |j8                  }t;        t=        j>                  |jA                                     }tC        |d      r%|jD                  rt        |jD                  xs g       ng }|r||d<   n|t        k7  r||d<   tG        |      }tI        ||       tK        |      jM                  d       t        d|        |rtO                dtQ        |       dntO                d}t        d|        y# t6        $ r9}t        d|        t        d       t        j                  d       Y d}~5d}~ww xY w)zExchange an auth code (or pasted redirect URL) for a refresh token.

    ``email`` selects the destination token path. ``None`` writes to the
    legacy single-user path (kept for the existing CLI entrypoint and for
    pre-multi-user installs).
    r   r   r   zKERROR: OAuth state mismatch. Run --auth-url again to start a fresh session.r   r   r   r   scoper   r   r   )r   r   r   r   1OAUTHLIB_RELAX_TOKEN_SCOPE)r   zERROR: Token exchange failed: z=The code may have expired. Run --auth-url to get a fresh URL.Ngranted_scopesr   T
missing_okz"OK: Authenticated. Token saved to z/google_chat_user_tokens/r4   z/google_chat_user_token.jsonzProfile path: ))r8   rB   r   r   r   r   r   r   r   r   r   r   r   listr<   
isinstancer   r   r   r   r   splitr   r   r   r   fetch_tokenrI   rY   rl   rm   rn   ro   hasattrr   r6   rk   r:   r   r	   r&   )r   r$   pending_authraw_callbackreturned_stater   r   r   r   r   	scope_valr   rQ   rP   token_payloadactually_grantedrO   	rel_labels                     r   exchange_auth_coder     s|     '')JK%e,LL248D..L,AA	
 	N./&\N,$)@)@)H(<0667ZZ(0RD!4::<	&__.N((!"!%%nmD7#"?3 ) D36

/0d# E6tzz%--/7RSM 5*+0D0D 	U!!'R( 
 "2h	6	!"0hU#J
M2u$$$5	.zl
;<   
!!:?5;Q:RRWX#%&&BC 
 
N9+
&'9  .se45MNs   %I6 6	J8?.J33J8c                   t        |       }|j                         st        d       yt                ddlm} ddlm} 	 |j                  t        |      t              }|j                  r"|j                  r|j                   |              ddl}|j                  j!                  |j                  j                  d|j"                   ddd	i
      d       t        d       |j'                  d       t)        |       j'                  d       t        d|        y# t$        $ r}t        d|        Y d}~Xd}~ww xY w)zRevoke the stored token with Google and delete it locally.

    Per-user when ``email`` given, legacy single-user when omitted.
    zNo token to revoke.Nr   r>   r@   z+https://oauth2.googleapis.com/revoke?token=POSTzContent-Typez!application/x-www-form-urlencoded)methodheaders   )timeoutzToken revoked with Google.z9Remote revocation failed (token may already be invalid): Tr   zDeleted )r6   rB   r   r   rC   r?   rD   rA   rH   r   r<   rK   rL   rM   urllib.requestrequesturlopentokenrI   r   r:   )r$   rO   r?   rA   rP   urllibrQ   s          r   revoker   Q  s$   
 U#J#$N56Q55c*ovN==U00MM')$NN""=ekk]K')LM # 
  	 	
 	*+ &u$$$5	HZL
!"  QI#OPPQs   B$D 	E (D;;E c                 \   t        j                  d      } | j                  d      }|j                  ddd       |j                  d	d
d       |j                  ddd       |j                  ddd       |j                  ddd       |j                  ddd       | j                  ddd d       | j	                         }|j
                  xs d }|j                  r(t        j                  t        |      rd       y d       y |j                  rt        |j                         y |j                  rt        |       y |j                  rt        |j                  |       y |j                   rt!        |       y |j"                  r"t        j                  t#               rdnd       y y )NzDGoogle Chat user-OAuth setup for Hermes (native attachment delivery))descriptionT)requiredz--check
store_truez)Check if auth is valid (exit 0=yes, 1=no))actionhelpz--client-secretPATHzStore OAuth client_secret.json)metavarr  z
--auth-urlz!Print OAuth URL for user to visitz--auth-codeCODEzExchange auth code for tokenz--revokezRevoke and delete stored tokenz--install-depszInstall Python dependenciesz--emailEMAILzMScope operation to a specific user's token (default: legacy single-user path))r  defaultr  r   r   )argparseArgumentParseradd_mutually_exclusive_groupadd_argument
parse_argsr$   checkr   r   r   client_secretr   r   r   	auth_coder   r   r   )parsergroupargsr$   s       r   mainr  v  s   $$ZF ///>E	yG  I	(&<  >	|L?  A	}f:  <	z,<  >	'9  ;
	7DA  B DJJ$Ezzj'/Q/			D../	U	4>>51	u			ln!, 
r   __main__)returnr   )r  r   )r$   r   r  r   )N)r$   Optional[str]r  r   )r$   r  r  Optional[Any])rP   r   r$   r  r  r  )rP   r   r  r   )r  r;   )rP   r   rO   r   r  None)ru   rt   r  rt   )r   r   r   r   r  r  )r  r  )r  bool)r$   r  r  r  )r   r   r  r  )r   r   r   r   r$   r  r  r  )r$   r  r  rt   )r   r   r  zTuple[str, Optional[str]])r$   r  r  r  )r   r   r$   r  r  r  )>__doc__
__future__r   r
  rm   loggingr   rer   r   
subprocessr   pathlibr   typingr   r   r   r   	getLoggerrF   hermes_constantsr	   r
   ModuleNotFoundErrorrE   utilsr   r   compiler!   r&   r)   r,   r.   r2   r6   r8   r:   r<   __annotations__r   r   rR   rT   r]   rh   rN   rl   rk   r   r   r   r   r   r   r   r   r   r   r  __name__r   r   r   <module>r*     sf  6p #    	 	    
  - - 
		E	FE  ! rzz+,"
:6B= B" ;	 
  %9xBQ&
&@2&2: .2*6:$$ 6H(V"#J"-J zF Y 	[) =s   D
 
DD