
    i8                   ~   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	m
Z ddlmZmZmZmZmZmZ 	 ddlZddlmZ ddlmZ ddlmZ dd	lmZ dd
lmZ ddl m!Z! ddl"m#Z# dZ$n# e%$ r dZ$dZdZdZdZdZdZe&Z!dZ#Y nw xY wddl'm(Z(m)Z)  e(d           ddl*m+Z+ ddl,m-Z-m.Z.m/Z/m0Z0m1Z1m2Z2m3Z3m4Z4m5Z5  ej6        d          Z7 ej8        d          Z9ddgZ:dZ;dZ<dZ=dZ>dZ?dZ@ eAh d          ZBd<d"ZCd#ZDd=d$ZEd%ZFd>d(ZGd?d*ZHd@d-ZI G d. d/          ZJ G d0 d1e-          ZKdAd4ZLd=d5ZMdAd6ZNdBd8ZOdCd:ZPdCd;ZQdS )Du  
Google Chat platform adapter.

Uses Google Cloud Pub/Sub (pull subscription) for inbound events and the
Google Chat REST API for outbound messages. Pattern parallels Slack Socket
Mode and Telegram long-polling: no public endpoint required.

Concurrency model
-----------------
The Pub/Sub SubscriberClient invokes its message callback in a background
thread (managed by the client's internal executor). The adapter's
``handle_message`` coroutine must run on the asyncio event loop, so the
callback uses ``asyncio.run_coroutine_threadsafe`` with
``add_done_callback`` (never ``.result()`` — that would block the callback
thread and saturate the Pub/Sub executor under load).

All outbound Chat REST calls go through ``asyncio.to_thread`` because the
googleapiclient is synchronous. This keeps the event loop responsive.

Pub/Sub delivery diagram::

    Pub/Sub stream   ->  callback thread        ->  asyncio loop
    (streaming_pull)     (_on_pubsub_message)       (handle_message)
         |                       |                        |
         |   at-least-once       |  parse + dedup         |  agent work
         |   delivery            |  _submit_on_loop       |  send() response
         |                       |  message.ack()         |
         v                       v                        v

Event type routing
------------------
Inbound envelope carries ``type`` in [MESSAGE, ADDED_TO_SPACE, REMOVED_FROM_SPACE,
CARD_CLICKED]. Only MESSAGE dispatches to the agent. ADDED_TO_SPACE caches the
bot's resource name (belt-and-suspenders on top of eager resolution in connect()).
CARD_CLICKED is ACK'd only in v1 (follow-up PR implements interactivity).
    )annotationsN)Path)AnyCallableDictListOptionalTuple)	pubsub_v1)
exceptions)service_account)AuthorizedHttp)build)	HttpError)MediaFileUploadTF)PlatformPlatformConfiggoogle_chat)MessageDeduplicator)	BasePlatformAdapterMessageEventMessageTypeProcessingOutcome
SendResultcache_audio_from_bytescache_document_from_bytescache_image_from_bytescache_video_from_byteszgateway.platforms.google_chatz:^projects/(?P<project>[^/]+)/subscriptions/(?P<sub>[^/]+)$z(https://www.googleapis.com/auth/chat.botz&https://www.googleapis.com/auth/pubsub        g      ?g       @g333333?>             excBaseExceptionreturnboolc                   t          | dd          }t          |dd          }t          |t                    r	|t          v S t	          |                                           }d|v sd|v rdS d|v rd|v sd	|v sd
|v rdS d|v sd|v rdS dS )u  Classify outbound API errors as transient (retryable) vs permanent.

    Retries are applied to:
      - HTTP 429 (rate-limited)
      - HTTP 5xx (server errors)
      - Network/transport failures (timeout, connection reset, DNS)

    Authentication errors (401/403), client errors (4xx other than 429),
    and well-formed non-retryable failures are NOT retried — those
    indicate a misconfiguration or revoked token, not a hiccup.
    respNstatustimeoutz	timed outT
connectionresetrefusedabortedzbroken pipezremote disconnectedF)getattr
isinstanceint_RETRYABLE_HTTP_STATUSESstrlower)r'   r,   r-   texts       J/home/piyush/.hermes/hermes-agent/plugins/platforms/google_chat/adapter.py_is_retryable_errorr;      s     3%%DT8T**F&# 2111 s88>>DDK4//ttDI4E4EVZIZIZt 5 = =t5    z
<consumed>c                     t           S )z9Check if Google Chat optional dependencies are installed.)GOOGLE_CHAT_AVAILABLE r<   r:   check_google_chat_requirementsr@      s      r<   )zgoogleapis.comzchat.google.comzdrive.google.comzdocs.google.comzlh3.googleusercontent.comzlh4.googleusercontent.comzlh5.googleusercontent.comzlh6.googleusercontent.comurlr7   c                    	 ddl m}  ||           }n# t          $ r Y dS w xY w|j        dk    rdS |j        pd                                sdS t          fdt          D                       S )zAReturn True iff *url* is https and targets a Google-owned domain.r   )urlparseFhttps c              3  T   K   | ]"}|k    p                     d |z             V  #dS ).N)endswith).0hhosts     r:   	<genexpr>z(_is_google_owned_host.<locals>.<genexpr>   s<      VVqtqy2DMM#'22VVVVVVr<   )urllib.parserC   	Exceptionschemehostnamer8   any_TRUSTED_ATTACHMENT_HOSTS)rA   rC   parsedrK   s      @r:   _is_google_owned_hostrT      s    ))))))#   uu}uO!r((**D uVVVV<UVVVVVVs    
##r9   c                    | s| S t          j        dd|           } t          j        dd|           } t          j        dd|           } | S )a  Sanitize subscription paths and email-like tokens from an error string.

    Covers project IDs leaking via Pub/Sub exception messages, plus SA-ish
    email addresses. agent/redact.py handles log-level redaction elsewhere;
    this helper is for user-facing error messages.
    z&projects/[^/\s]+/subscriptions/[^/\s]+z,projects/<redacted>/subscriptions/<redacted>zprojects/[^/\s]+/topics/[^/\s]+z%projects/<redacted>/topics/<redacted>z9[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.iam\.gserviceaccount\.comz&<sa>@<project>.iam.gserviceaccount.com)resub)r9   s    r:   _redact_sensitiverX      sj      616 D
 6*/ D
 6D0 D
 Kr<   mimer   c                    | st           j        S |                     d          rt           j        S |                     d          rt           j        S |                     d          rt           j        S t           j        S )zMap a MIME string to a hermes MessageType.

    Anything not image/audio/video falls through to DOCUMENT so the agent
    still receives the file.
    image/audio/video/)r   DOCUMENT
startswithPHOTOAUDIOVIDEO)rY   s    r:   _mime_for_message_typerc      su      $##x   !  x   !  x   !  r<   c                  :    e Zd ZdZddZddZddZddZddZdS )_ThreadCountStoreuR  Per-(chat_id, thread_name) inbound message counter, persisted to disk.

    Drives the DM main-flow vs side-thread heuristic:

    - prev_count == 0 (first time we see this thread) → "main flow":
      Google Chat just auto-created a fresh thread for the user's
      top-level message. Treat it as part of the shared DM session;
      bot replies at top-level (no thread.name on outbound).
    - prev_count >= 1 (we've already seen this thread) → "side thread":
      user explicitly engaged a thread that's been around. Isolate
      session by thread, route bot reply into the same thread.

    Persistence is essential: without it, every gateway restart wipes
    counts and active side-threads silently demote to "main flow",
    which leaks main-flow context into the user's isolated thread
    (the bug Ramón reported across 4 iterations of the in-memory
    version).

    File format (JSON):
        {"<chat_id>": {"<thread_name>": <int_count>, ...}, ...}

    Failure modes are non-fatal: a missing or corrupt file resets to
    empty (logged as warning) so the adapter never crashes on disk
    issues. The next ``incr`` will write a fresh file.

    Save strategy: write-through after every ``incr``. The file is
    tiny (a few KB even for very active bots), so the simplicity of
    write-through outweighs the cost of debouncing for now.
    path_Pathc                0    || _         i | _        d| _        d S NF)_path_counts_loaded)selfrf   s     r:   __init__z_ThreadCountStore.__init__(  s    
24r<   r)   Nonec                L   d| _         | j                                        s	i | _        dS 	 | j                                        }|                                rt          j        |          ni }n# t          j        $ r3}t          
                    d| j        |           i | _        Y d}~dS d}~wt          $ r3}t          
                    d| j        |           i | _        Y d}~dS d}~ww xY wi }t          |t                    r|                                D ]\  }}t          |t                    rt          |t                    s0i }|                                D ]4\  }}	t          |t                    rt          |	t                     r|	||<   5|r|||<   || _        dS )u   Load counts from disk. Safe to call multiple times.

        Missing file → empty store. Corrupt JSON → empty store + warn.
        TNzD[GoogleChat] thread-count store at %s is corrupt; starting fresh: %sz8[GoogleChat] could not read thread-count store at %s: %s)rl   rj   existsrk   	read_textstripjsonloadsJSONDecodeErrorloggerwarningOSErrorr4   dictitemsr7   r5   )
rm   rawdatar'   cleanchat_idthreadsclean_threadsthread_namecounts
             r:   loadz_ThreadCountStore.load-  s   
 z  "" 	DLF	*&&((C&)iikk94:c???rDD# 	 	 	NN%
C  
 DLFFFFF 	 	 	NNJ
C   DLFFFFF	 ,.dD!! 		3$(JJLL 3 3 !'3// z'47P7P 02*1--// ; ;&K!+s33 ;
5#8N8N ;5:k2  3%2E'Ns$   AA/ /C,>(B,,C,9(C''C,r   r7   r   r5   c                `    | j                             |i                               |d          S )z:Return the current count for (chat_id, thread_name), or 0.r   )rk   get)rm   r   r   s      r:   r   z_ThreadCountStore.getV  s*    |,,00a@@@r<   c                    | j                             |i           }|                    |d          }|dz   ||<   |                                  |S )u   Increment count and write through to disk. Returns the
        PRE-increment value (the heuristic input — "have we seen this
        thread before this message?").r      )rk   
setdefaultr   _save)rm   r   r   chat_countsprevs        r:   incrz_ThreadCountStore.incrZ  sN     l--gr::{A..#'!8K 

r<   c                   	 | j         j                            dd           | j                             | j         j        dz             }|                    t          j        | j        d                     t          j
        || j                    dS # t          $ r,}t                              d| j         |           Y d}~dS d}~ww xY w)u   Atomic write of the counts dict to disk.

        Failure is non-fatal — log warning and continue. The in-memory
        counts stay consistent within the running process; only restart
        recovery is affected.
        Tparentsexist_okz.tmp),:)
separatorsz;[GoogleChat] could not persist thread-count store to %s: %sN)rj   parentmkdirwith_suffixsuffix
write_textrt   dumpsrk   osreplacery   rw   rx   )rm   tmpr'   s      r:   r   z_ThreadCountStore._saved  s    		J##D4#@@@*(():V)CDDCNN4:dlzJJJKKKJsDJ''''' 	 	 	NNM
C        	s   BB 
C
!CC
N)rf   rg   r)   ro   )r   r7   r   r7   r)   r5   )	__name__
__module____qualname____doc__rn   r   r   r   r   r?   r<   r:   re   re   	  s         <   
' ' ' 'RA A A A        r<   re   c                  `    e Zd ZdZeZdZdZdZd} fdZ	d~d
Z
ddZedd            Zedd            ZddZddZddZddZddZddZddZdd Ze	 ddd&            Zdd(Zdd*Z	 ddd0Zdd2Zdd5Z	 	 ddd;Zd<d=dd@ZddAZddDZ ddGZ! e"j#        dH          Z$e%ddI            Z&	 dddJZ'd~dKZ(dLdMddQZ)ddRZ*dddSZ+ddTZ,ddYZ-dd[Z.	 	 	 ddd^Z/	 	 dddaZ0	 	 	 ddddZ1	 	 dddfZ2	 	 dddhZ3	 	 	 dddjZ4eddm            Z5dnZ6ddqZ7ddsZ8dduZ9	 	 dddyZ:dd{Z;dd|Z< xZ=S )GoogleChatAdapteraZ  
    Google Chat bot adapter using Pub/Sub pull + Chat REST API.

    Required environment (see gateway/config.py Google Chat block):
      GOOGLE_CHAT_PROJECT_ID           (or GOOGLE_CLOUD_PROJECT fallback)
      GOOGLE_CHAT_SUBSCRIPTION_NAME    (or GOOGLE_CHAT_SUBSCRIPTION fallback)
      GOOGLE_CHAT_SERVICE_ACCOUNT_JSON (or GOOGLE_APPLICATION_CREDENTIALS)

    Optional:
      GOOGLE_CHAT_ALLOWED_USERS, GOOGLE_CHAT_ALLOW_ALL_USERS
      GOOGLE_CHAT_HOME_CHANNEL
      GOOGLE_CHAT_MAX_MESSAGES (FlowControl, default 1)
      GOOGLE_CHAT_MAX_BYTES    (FlowControl, default 16_777_216 = 16 MiB)
    
   g       @g      ^@configr   c                   t                                          |t          d                     d | _        d | _        d | _        d | _        i | _        i | _        i | _	        d | _
        d | _        d | _        d | _        d | _        d | _        d | _        t#                      | _        i | _        d| _        i | _        i | _        	 ddlm}  |            }n-# t2          t4          f$ r t7          j                    dz  }Y nw xY wt;          |dz            | _        i | _        i | _         tC          tE          j#        dd                    | _$        tC          tE          j#        d	tK          d
                              | _&        d S )Nr   Fr   )get_hermes_home.hermeszgoogle_chat_thread_counts.jsonGOOGLE_CHAT_MAX_MESSAGES1GOOGLE_CHAT_MAX_BYTESi   )'superrn   r   _subscriber	_chat_api_user_chat_api_user_credentials_user_creds_by_email_user_chat_api_by_email_last_sender_by_chat_credentials_project_id_subscription_path_streaming_pull_future_supervisor_task_loop_bot_user_idr   _dedup_typing_messages_shutting_down_rate_limit_hits_last_inbound_threadhermes_constantsr   ModuleNotFoundErrorImportErrorrg   homere   _thread_count_store_typing_card_inflight_orphan_typing_messagesr5   r   getenv_max_messagesr7   
_max_bytes)rm   r   _get_hermes_home_hermes_home	__class__s       r:   rn   zGoogleChatAdapter.__init__  s   
 	-!8!8999*.(," .20446!79$ 57!+/*.1559#8<:>
+/)++02#02 57!	4LLLLLL++--LL#[1 	4 	4 	4 :<<)3LLL	4#4;;$
 $
  @B"
 >@$ +Es!K!KLLbi(?EUAVAVWWXXs   C 'C>=C>r)   r   c                   | j         j                            d          pt          j        d          }|rK|                                                    d          rf	 t          j        |          }n*# t          j	        $ r}t          d|           |d}~ww xY wt          j                            |t                    S t          j                            |          st#          d          	 t%          |dd	
          5 }t          j        |          }ddd           n# 1 swxY w Y   n*# t          j	        $ r}t          d|           |d}~ww xY wt          j                            |t                    S 	 ddlm} n# t,          $ r d}Y nw xY w|t          d          	 |                    t                    \  }}n%# t0          $ r}t          d|           |d}~ww xY wt2                              d           |S )u  Load Service Account credentials from env or config.extra,
        falling back to Application Default Credentials.

        Priority:
          1. Explicit ``extra['service_account_json']`` (path or inline JSON)
          2. ``GOOGLE_APPLICATION_CREDENTIALS`` env var (path)
          3. Application Default Credentials via ``google.auth.default()``
             — works on Cloud Run / GCE / GKE with a workload identity
             attached, or locally via ``gcloud auth application-default
             login``. Lets operators run the gateway in GCP without
             managing SA key files. Pattern lifted from PR #14965.
        service_account_jsonGOOGLE_APPLICATION_CREDENTIALS{z"Inline SA JSON is not valid JSON: N)scopesz7Service Account JSON file not found at configured path.rutf-8encodingz-Service Account JSON file is not valid JSON: r   zNo Service Account credentials configured. Set GOOGLE_CHAT_SERVICE_ACCOUNT_JSON or GOOGLE_APPLICATION_CREDENTIALS, or install google-auth to use Application Default Credentials.zNo Service Account credentials configured and Application Default Credentials are unavailable. Set GOOGLE_CHAT_SERVICE_ACCOUNT_JSON or run ``gcloud auth application-default login``. ADC error: zI[GoogleChat] No SA JSON configured; using Application Default Credentials)r   extrar   r   r   lstripr_   rt   ru   rv   
ValueErrorr   Credentialsfrom_service_account_info_CHAT_SCOPESrf   rq   FileNotFoundErroropenr   google.authauthr   defaultrN   rw   info)rm   sa_pathr   r'   fhgoogle_authcredentials_projects           r:   _load_sa_credentialsz&GoogleChatAdapter._load_sa_credentials  s    K!!"899 ;y9:: 	  	~~**3// 	:g..DD+   $BSBB  '2LL M    7>>'** 'N  '3999 )R9R==D) ) ) ) ) ) ) ) ) ) ) ) ) ) )'    ICII  #.HH\ I   	------- 	 	 	KKK	Q  
		$/$7$7|$7$L$L!K 	 	 	$ "	$ $  	 	"	
 	
 	
 s~   A4 4BBB3D2 D&D2 &D**D2 -D*.D2 2EEEF
 
FF.G 
G/G**G/Tuple[str, str]c                p   | j         j                            d          }| j         j                            d          }|st          d          |st          d          t                              |          }|st          d          |                    d          |k    rt          d          ||fS )zReturn (project_id, subscription_path) after validation.

        Raises ValueError with a sanitized message on any config problem.
        
project_idsubscription_namez<GOOGLE_CHAT_PROJECT_ID (or GOOGLE_CLOUD_PROJECT) is not set.zGGOOGLE_CHAT_SUBSCRIPTION_NAME (or GOOGLE_CHAT_SUBSCRIPTION) is not set.zRGOOGLE_CHAT_SUBSCRIPTION_NAME must match 'projects/<project>/subscriptions/<sub>'.projectzjproject_id in GOOGLE_CHAT_PROJECT_ID does not match the project embedded in GOOGLE_CHAT_SUBSCRIPTION_NAME.)r   r   r   r   _SUBSCRIPTION_PATH_REmatchgroup)rm   r   subscriptionr   s       r:   _validate_configz"GoogleChatAdapter._validate_config.  s    
 [&**<88
{(,,-@AA 	N    	Y   &++L99 	<   ;;y!!Z//E   <''r<   futurero   c                    	 |                                   d S # t          $ r t                              d           Y d S w xY w)Nz1[GoogleChat] Background inbound processing failed)resultrN   rw   	exception)r   s    r:   _log_background_failurez)GoogleChatAdapter._log_background_failureM  sV    	RMMOOOOO 	R 	R 	RPQQQQQQ	Rs    $A A loop#Optional[asyncio.AbstractEventLoop]r*   c                Z    | d uo't           t          | dd                                  S )N	is_closedc                     dS ri   r?   r?   r<   r:   <lambda>z;GoogleChatAdapter._loop_accepts_callbacks.<locals>.<lambda>V  s    PU r<   )r*   r3   )r   s    r:   _loop_accepts_callbacksz)GoogleChatAdapter._loop_accepts_callbacksT  s4    4Y-VWT;-V-V-X-X(Y(Y$YYr<   coroc                .   | j         }|                     |          st                              d           dS 	 t	          j        ||          }n+# t          $ r t                              d           Y dS w xY w|                    | j                   dS )zHSchedule a coroutine on the adapter loop from a Pub/Sub callback thread.z9[GoogleChat] Loop not accepting callbacks; dropping eventNz1[GoogleChat] Loop closed between check and submit)	r   r   rw   rx   asynciorun_coroutine_threadsafeRuntimeErroradd_done_callbackr   )rm   r   r   r   s       r:   _submit_on_loopz!GoogleChatAdapter._submit_on_loopX  s    z++D11 	 NNVWWWF	5dDAAFF 	 	 	NNNOOOFF	 	  !=>>>>>s   A $A87A8rg   c                    t          j        dt          t          j                    dz                      }t          |          dz  S )zBLocation where the resolved bot user_id is cached across restarts.HERMES_HOMEr   zgoogle_chat_bot_id.json)r   r   r7   rg   r   )rm   bases     r:   _bot_id_cache_pathz$GoogleChatAdapter._bot_id_cache_pathj  s8    yEJLL9,D(E(EFFT{{666r<   Optional[str]c                   |                                  }|                                sd S 	 t          j        |                    d                    }|                    d          pd S # t          t          j        f$ r Y d S w xY w)Nr   r   bot_user_id)r	  rq   rt   ru   rr   r   ry   rv   )rm   rf   r}   s      r:   _load_cached_bot_idz%GoogleChatAdapter._load_cached_bot_ido  s    &&(({{}} 	4	:dnngn>>??D88M**2d2-. 	 	 	44	s   >A+ +BBr  r7   c                   	 |                                  }|j                            dd           |                    t	          j        d|i          d           d S # t          $ r  t                              dd           Y d S w xY w)NTr   r  r   r   z0[GoogleChat] Could not persist bot_user_id cacheexc_info)	r	  r   r   r   rt   r   ry   rw   debug)rm   r  rf   s      r:   _save_cached_bot_idz%GoogleChatAdapter._save_cached_bot_idy  s    	\**,,DKdT:::OO
M;788         	\ 	\ 	\LLKVZL[[[[[[	\s   AA &B	B	c                   K   g } j         j        r5 j         j        j        r$|                     j         j        j                   t	          j        dd                                          }|r2|                    d |                    d          D                        |D ]}	 t          j
        |f fd	           d{V }nM# t          $ r@}t                              d|t          t          |                               Y d}~hd}~ww xY w|                    dg           D ]`}|                    d	i                               d
          dk    r1|                    d	i                               d          }|r|c c S adS )ud  Resolve ``users/{id}`` via Chat API members.list on a known space.

        Tries the home channel first, then any space from the allowlist.
        If no space is known, returns None and self-filter falls back to
        filtering ``sender.type == 'BOT'`` (which is still safe but less
        precise — own messages and other bots look alike).
        GOOGLE_CHAT_BOOTSTRAP_SPACESrE   c              3  f   K   | ],}|                                 |                                 V  -d S N)rs   )rI   ss     r:   rL   z9GoogleChatAdapter._resolve_bot_user_id.<locals>.<genexpr>  sK       $ $aggii$		$ $ $ $ $ $r<   r   c                    j                                                                                             | d                                                                        S )N2   )r   pageSizehttp)r   spacesmemberslistexecute_new_authed_http)r  rm   s    r:   r   z8GoogleChatAdapter._resolve_bot_user_id.<locals>.<lambda>  sM    DN$9$9$;$;WYYTRT00W$"7"7"9"9W:: r<   Nz*[GoogleChat] members.list failed on %s: %smembershipsmembertypeBOTname)r   home_channelr   appendr   r   rs   extendsplitr  	to_threadr   rw   r  rX   r7   r   )rm   candidate_spacesextra_spacesspacer  r'   r#  r&  s   `       r:   _resolve_bot_user_idz&GoogleChatAdapter._resolve_bot_user_id  s      ');# 	F(@(H 	F##DK$<$DEEEy!?DDJJLL 	## $ $#/#5#5c#:#:$ $ $    & 	$ 	$E ' 1" ; ; ; ; ;! !          @%c#hh//  
  "++mR88 $ $::h++//775@@!::h3377??D $#	$
 ts   'C
D6DDc           	     	   	
K   t           s                     ddd           dS t          j                     _        	                                  \  }	                                 nn# t          t          f$ rZ}t          t          |                    }t                              d|                                d|d           Y d}~dS d}~ww xY w| _        	 _         _        	 t          j        fd           d{V  _        ng# t$          $ rZ}t          t          |                    }t                              d	|                                d
|d           Y d}~dS d}~ww xY w	 ddlm}mm} t          j        |           d{V 

D
 _        t          j        
fd           d{V  _        t                              d           t          j        |           d{V }|r=t                              dt5          |          d                    |                     n
t                              d           nZ# t$          $ rM}t                              dt          t          |                               d _        d _        Y d}~nd}~ww xY w	 t          j         j        j                   d{V  n,# t$          $ r t                              dd           Y nw xY wt?          j                    _!        	 t          j         	fd           d{V  n# tD          j#        $ r                      ddd           Y dS tD          j$        $ r                      ddd           Y dS t$          $ rZ}t          t          |                    }t                              d|                                d|d           Y d}~dS d}~ww xY w %                                 _&         j&        s[ '                                 d{V  _&         j&        r (                     j&                   nt                              d           t          j)         *                                           _+         ,                                 t                              d| j&        pd  j-         j.                   dS )!zBValidate config, authenticate, start Pub/Sub pull, resolve bot id.missing_depsz<google-cloud-pubsub / google-api-python-client not installedFcodemessage	retryablez)[GoogleChat] Config validation failed: %sconfig_invalidNc                 *    t          dd d          S )Nchatv1F)r   cache_discovery)build_servicer   s   r:   r   z+GoogleChatAdapter.connect.<locals>.<lambda>  s"     +$)	   r<   z0[GoogleChat] Failed to build Chat API client: %schat_api_initr   )load_user_credentialsbuild_user_chat_servicelist_authorized_emailsc                                 S r  r?   )_build_user_chat
user_credss   r:   r   z+GoogleChatAdapter.connect.<locals>.<lambda>  s    ,,Z88 r<   uN   [GoogleChat] Legacy user OAuth loaded — fallback attachment delivery enabledz1[GoogleChat] %d per-user OAuth tokens on disk: %sz, u   [GoogleChat] No user OAuth tokens at setup — file attachments will degrade to text-only fallback. Each user runs /setup-files once in their own DM to enable native attachments.zX[GoogleChat] User OAuth load failed (attachments will degrade to text-only fallback): %szK[GoogleChat] thread-count store load failed (treating all threads as fresh)Tr  r<  c                 >     j                             di          S )Nr   )request)r   get_subscription)rm   subscription_paths   r:   r   z+GoogleChatAdapter.connect.<locals>.<lambda>  s(    (99+->? :   r<   subscription_not_foundz1Pub/Sub subscription not found at configured pathsubscription_permissionzAService Account lacks roles/pubsub.subscriber on the subscriptionz([GoogleChat] subscription.get failed: %ssubscription_checkz^[GoogleChat] bot_user_id not yet resolved; will resolve on first addedToSpace or member lookupzl[GoogleChat] Connected; project=%s, subscription=<redacted>, bot_user_id=%s, flow_control(msgs=%s, bytes=%s)z<unresolved>)/r>   _set_fatal_errorr  get_running_loopr   r   r   r   r   rX   r7   rw   errorr   r   r   r+  r   rN   oauthr>  r?  r@  r   r   r   lenjoinrx   r   r   r   SubscriberClientr   gax_exceptionsNotFoundPermissionDeniedr  r   r/  r  create_task_run_supervisorr   _mark_connectedr   r   )rm   r   r'   msg_load_user_creds_list_emails
authorizedrB  r   rG  rC  s   `      @@@@r:   connectzGoogleChatAdapter.connect  sf     $ 	!!#V "   
 5-//
	,0,A,A,C,C)J)3355KK-. 	 	 	#CHH--CLLDcJJJ!!'7PU!VVV55555		 &"3'	#*#4   $ $      DNN  	 	 	#CHH--CLLKSQQQ!!u!UUU55555		$	'         
  '01ABBBBBBBBJ%)3&,3,=88888- - ' ' ' ' ' '# 2    '0>>>>>>>>J G
OOTYYz%:%:    #4    	' 	' 	'NN5!#c((++  
 &*D""&D	'	#D$<$ABBBBBBBBBB 	 	 	NN(26      	 %5+NNN	#             
 & 	 	 	!!-K "   
 55. 		 		 		!!.#   "    55 	 	 	#CHH--CLLCSIII!!';STX!YYY55555		 !4466  	&*&?&?&A&A A A A A A AD  (():;;;;J   !( 3D4H4H4J4J K K>/O	
 	
 	
 ts   +A. .C?ACC2"D 
E9AE44E9=CI 
J4'AJ//J48$K &LL$M 'O9-&O9	O9AO44O9c                  K   d| _         | j        rv| j                                        s]| j                                         	 t	          j        | j        d           d{V  n!# t          j        t          j        f$ r Y nw xY w| j        W	 | j                                         t	          j	        | j        j
        d           d{V  n# t          $ r Y nw xY wd| _        | j        =	 t	          j	        | j        j                   d{V  n# t          $ r Y nw xY wd| _        |                                  t                              d           dS )zKClean shutdown: stop accepting new messages, wait in-flight, close clients.T      @r.   Ng      $@z[GoogleChat] Disconnected)r   r   donecancelr  wait_forCancelledErrorTimeoutErrorr   r+  r   rN   r   close_mark_disconnectedrw   r   rm   s    r:   
disconnectzGoogleChatAdapter.disconnectF  s     "  	)>)C)C)E)E 	!((***&t'<cJJJJJJJJJJJ*G,@A   &2+22444'(C(JDQQQQQQQQQQ   *.D'''(8(>??????????   #D!!!/00000s6   !A& &BB>C 
CC-$D 
DDc                0  K   d}| j         s	t          j                            | j        | j                  }	 | j                            | j        | j	        |          }|| _
        |dk    rt                              d|           d}t          j        |j                   d{V  | j         rdS nX# t          j        $ r Y dS t"          j        $ r |                     ddd	           Y dS t"          j        $ r |                     d
dd	           Y dS t*          $ r}|dz  }t-          t/          |                    }t                              d|| j        |           || j        k    r"|                     dd| dd	           Y d}~dS t5          | j        | j        d|dz
  z  z            }t;          j        d|          }	 t          j        |           d{V  n# t          j        $ r Y Y d}~dS w xY wY d}~nd}~ww xY w| j         dS dS )a  Run the streaming_pull with exponential backoff; fatal after 10 attempts.

        ``subscribe()`` returns a concurrent.futures.Future that resolves when
        the stream dies. We await ``future.result()`` in a worker thread and
        react to exceptions.
        r   )max_messages	max_bytes)callbackflow_controlz9[GoogleChat] Pub/Sub stream reconnected after %d attemptsNpubsub_authz6Pub/Sub authentication failed (SA key invalid/revoked)Fr2  pubsub_permissionz.SA lacks pubsub.subscriber on the subscriptionr   z4[GoogleChat] Pub/Sub stream died (attempt %d/%d): %spubsub_reconnect_exhaustedzPub/Sub reconnect failed z times; giving up   ) r   r   typesFlowControlr   r   r   	subscriber   _on_pubsub_messager   rw   r   r  r+  r   rc  rR  UnauthenticatedrK  rT  rN   rX   r7   rx   _MAX_RECONNECT_ATTEMPTSmin_RECONNECT_MAX_DELAY_RECONNECT_BASE_DELAYrandomuniformsleep)rm   attemptflowr   r'   rX  delay	sleep_fors           r:   rV  z!GoogleChatAdapter._run_supervisorb  s      % =	?..!// /  D8)33+!4!% 4  
 /5+Q;;KK []deee'666666666& F)   !1   %%&T# &   
 !2   %%,L# &   
    1'C11J0	   d:::))9 VG V V V"' *   
 FFFFF-.!!2DE 
 #N1e44	!-	2222222222-   FFFFFF 32222/I % =	 =	 =	 =	 =	sU   A7B4 4H	&H	/&H		H	!A*H8H
G%$H%G<4H;G<<HH	rE   envelopeDict[str, Any]ce_type4Optional[Tuple[Dict[str, Any], Dict[str, Any], str]]c                <   |                      d          pi }|r|                     d          nd}|rH|                     d          pi }|                     d          p|                     d          pi }||dfS t          |                      d          t                    rU|                      dd          d	k    rdS | d         }|                      d          p|                     d          pi }||d
fS d| v sd| v r|                      dd	          d	k    rdS |                      d          pd                                }|                      d          p|pd}d|pd                    dd                              dd          z   }|                      dd          pd}	|                      dd          pd|||dd|	|	d}|                      d          pd}
|
rd|
i|d<   |                      dd          pd|                      dd          d }||d!fS dS )"u  Detect Pub/Sub envelope format and return ``(message, space, format_name)``.

        Three known formats are accepted. Returns ``None`` when the envelope
        is unrecognized, is a non-MESSAGE event, or otherwise should be
        silently dropped.

        Format 1 — Workspace Add-ons (canonical, ce-type-driven)::

            {"chat": {"messagePayload": {"message": {...}, "space": {...}}}}

        Format 2 — Native Chat API Pub/Sub (alternative configuration where
        the Chat app publishes events directly without the Workspace
        Add-ons wrapper)::

            {"type": "MESSAGE", "message": {...}, "space": {...}}

        Format 3 — Relay / flat (a custom Cloud Run relay that flattens the
        Chat event into top-level fields)::

            {"event_type": "MESSAGE", "sender_email": "...", "text": "...",
             "space_name": "spaces/X", "thread_name": "spaces/X/threads/Y",
             "message_name": "spaces/X/messages/M.M"}

        For format 3 the helper synthesizes a Chat-API-shaped ``message``
        dict so downstream code (``_dispatch_message`` →
        ``_build_message_event``) can consume it without branching.
        r8  messagePayloadNr4  r.  workspace_addonsr$  rE   MESSAGEnative_chat_api
event_typesender_emailsender_display_nameUnknownzusers/relay-unknown@_at_rG   _r9   message_nameHUMAN)r&  emaildisplayNamer$  )r&  senderr9   argumentTextr   r&  thread
space_name
space_typeSPACE)r&  	spaceType
relay_flat)r   r4   rz   rs   r   )r  r  
chat_blockmsg_payload_wrapperrX  r.  r  sender_displaysender_name_surrogater9   r   s              r:   _extract_message_payloadz*GoogleChatAdapter._extract_message_payload  s   H \\&))/R
BLVjnn-=>>>RV 	2%)))44:C'++G44N8H8HNBE111
 hll9--t44 	1||FB''944t9%CLL))CSWWW-=-=CE000
 8##~'A'A||L)44	AAt$LL88>BEEGGL233   ,955c6BBJJ3PSTTU " <<++1rD ^R88>B1)#1#	   $
# 
#C #,,}55;K 6!' 5H \266<"%\\,@@ E |++tr<   r4  c                	   | j         r|                                 dS 	 t          j        |j                            d                    }n?# t          $ r2 t                              d           |	                                 Y dS w xY wt          t          |di           pi           }|                    d          pd}t                              dt          |                                          |           t!          j        d          rZ	 d	d
lm}  |t          j        |                    }n# t          $ r d}Y nw xY wt                              d|dd                    	 |                    d          pi }d|v sd|v r(|                    d          pi }|                    d          pi }	|                    d          pi }
d|v r|
                    d          pi }|                    d          dk    r:| j        s3|                    d          }|r|| _        |                     |           t                              d|	                    dd                     n/t                              d|	                    dd                     |	                                 dS d|v sd|                                v r0t                              d           |	                                 dS |                     ||          }|Qt                              d|t          |                                                     |	                                 dS |\  }}	}|                    d          pi }|                    d          pd}|dk    r|	                                 dS |                    d          pd}|rK| j                            |          r1t                              d |           |	                                 dS t          |          }d|vr|	r|	|d<   t          |          }d|vr|	r|	|d<   |                     |                     ||                     |	                                 dS # t          $ rE t                              d!           	 |	                                 Y dS # t          $ r Y Y dS w xY ww xY w)"u  Pub/Sub callback — parse envelope and dispatch to asyncio loop.

        Runs in a Pub/Sub SubscriberClient worker thread, NOT the event loop.
        Never block this function; never raise out of it (that triggers
        Pub/Sub nack + infinite redelivery).

        Google Chat Events API uses CloudEvents-style Pub/Sub messages. The
        event type is carried in Pub/Sub message attributes (``ce-type``),
        not in the JSON body. The body is wrapped in a ``chat`` object whose
        keys depend on the event type:

          - google.workspace.chat.message.v1.created
              -> envelope["chat"]["messagePayload"] = {space, message}
          - google.workspace.chat.membership.v1.created
              -> envelope["chat"]["membershipPayload"] = {space, membership}
          - google.workspace.chat.membership.v1.deleted
              -> envelope["chat"]["membershipPayload"] = {space, membership}
        Nr   z-[GoogleChat] Could not parse Pub/Sub envelope
attributeszce-typerE   z)[GoogleChat] Envelope keys=%s, ce-type=%sGOOGLE_CHAT_DEBUG_RAWr   )redact_sensitive_textz<redact filter unavailable>z([GoogleChat] RAW envelope (redacted): %si  r8  
membership
MEMBERSHIPmembershipPayloadr.  createdr#  r$  r%  r&  z[GoogleChat] ADDED_TO_SPACE %s?z"[GoogleChat] REMOVED_FROM_SPACE %swidgetcardz;[GoogleChat] Card/widget event ack'd (v2 feature, deferred)zO[GoogleChat] Envelope did not match a known message format; ce-type=%s, keys=%sr  z[GoogleChat] Dedup drop for %sz([GoogleChat] Error in _on_pubsub_message)r   nackrt   ru   r}   decoderN   rw   r   ackrz   r3   r   r  r  keysr   r   agent.redactr  r   r   r  r   r8   r  r   is_duplicater  _dispatch_message)rm   r4  r  attrsr  r  dumpr  mplr.  r  r#  r&  	extractedrX  _fmtr  sender_typemsg_namemsg_with_spaceenriched_envs                        r:   ru  z$GoogleChatAdapter._on_pubsub_message  s"   &  	LLNNNF	z',"5"5g">">??HH 	 	 	LMMMKKMMMFF	
 WWlB77=2>>))I&&,"7!!	
 	
 	

 9,-- 	R
5>>>>>>,,TZ-A-ABB 5 5 545LLCT%4%[QQQN	!f--3J w&&,'*A*A nn%899?R((.B WW\228b
'''^^H55;Fzz&))U224;L2%zz&11 ;04D- 44T:::KK8%))FC:P:P    KK<eiiPS>T>T    7""f&?&?Q    55hHHI *+2D4I4I   (CWWX&&,"F **V,,2K e## wwv,"H DK44X>> =xHHH "#YYNn,,,*/w'  >>Ll**u*(-W%  !7!7!U!UVVVKKMMMMM 	 	 	GHHH   		sq   ,A 8BB#D6 6EE,ER 4AR >A'R 'AR 7A"R A1R $S3S


SSSSrX  c                  K   	 |                      ||           d{V }|dS |j        pd                                }|                    d          r_|j        X|j        r|j        j        r|j        j        nd}|                     |j        j        |j        j        ||           d{V }|rdS | 	                    |           d{V  dS # t          $ r t                              d           Y dS w xY w)u  Translate a Chat message payload to a MessageEvent and hand off.

        Intercepts the ``/setup-files`` admin command BEFORE the agent
        sees it — that's a bot-local OAuth setup flow, not a prompt.
        Everything else flows to ``handle_message`` as normal.
        NrE   z/setup-files)r   	thread_idraw_textr  z%[GoogleChat] _dispatch_message failed)_build_message_eventr9   rs   r_   sourceuser_id_alt_handle_setup_files_commandr   r  handle_messagerN   rw   r   )rm   rX  r  eventr9   r  handleds          r:   r  z#GoogleChatAdapter._dispatch_message  sf     	F33CBBBBBBBBE} J$"++--D~.. 5<3K |(-(@EL,, 
 !% @ @!L0#l4!!-	 !A ! !        F%%e,,,,,,,,,,, 	F 	F 	FDEEEEEE	Fs   C BC 3C $C87C8Nr   r  r  r  c                l   K   ddl m |r&|                                                                nd}|                    d          }t          |          dk    r|d                                         nd}d) fd
}|s                                                                }	                    |          }
|
                                }|r	                    |          nd}||pd} |d| d|
 d           d{V  dS |	s |d           d{V  dS  |d           d{V  dS |dk    r]                                                                s |d           d{V  dS 	 ddl
}ddl}|                                }|                    |          5  t          j        j        |           d{V  ddd           n# 1 swxY w Y   |                                                                                                d         }nd# t&          $ r  |d           d{V  Y dS t(          $ r:}t*                              d|            |d|            d{V  Y d}~dS d}~ww xY w |d| d           d{V  dS |dk    rE	 ddl
}ddl}|                                }|                    |          5  t          j        j        |           d{V  ddd           n# 1 swxY w Y   |                                                                pd}nT# t&          $ r d}Y nFt(          $ r:}t*                              d|            |d|            d{V  Y d}~dS d}~ww xY w|r7 j                            |d            j                            |d           nd _        d _         |d | d!           d{V  dS 	 ddl
}ddl}|                                }|                    |          5  t          j        j        ||           d{V  ddd           n# 1 swxY w Y   |                                                                }nd# t&          $ r  |d"           d{V  Y dS t(          $ r:}t*                              d#|            |d|            d{V  Y d}~dS d}~ww xY w	 t          j        j	        |           d{V Vt          j        fd$           d{V }|r j        |<   | j        |<   n _        | _         |d%           d{V  dS n2# t(          $ r%}t*                              d&|           Y d}~nd}~ww xY w |d'                    |           d(| d!           d{V  dS )*uP  Run the in-chat OAuth setup flow for native attachment delivery.

        Returns ``True`` if the message was consumed (no agent dispatch),
        ``False`` if it should fall through.

        Multi-user mode: ``sender_email`` is the asker's identity, which
        is also the per-user OAuth key. ``status`` / ``start`` / ``revoke``
        / code-exchange all operate on THIS user's token slot. When
        ``sender_email`` is ``None`` (e.g. tests, or older inbound events
        without a populated email field) the handler falls back to the
        legacy single-user path so pre-multi-user installs keep working.

        Subcommands:
          /setup-files                  → show status + next step
          /setup-files start            → print OAuth URL
          /setup-files revoke           → revoke and delete stored token
          /setup-files <CODE_OR_URL>    → exchange auth code for token

        Pre-requisite: client_secret.json must already be on the host
        (one-time terminal step). The status reply tells the user how to
        do that if it's missing.
        r   )rN  N)maxsplitrE   r9   r7   r)   ro   c                   K   d| i}rdi|d<   	                      |           d {V  d S # t          $ r  t                              dd           Y d S w xY w)Nr9   r&  r  z+[GoogleChat] /setup-files reply send failedTr  )_create_messagerN   rw   r  )r9   bodyr   rm   r  s     r:   _replyz=GoogleChatAdapter._handle_setup_files_command.<locals>._reply  s      $*D>D 5"()!4X**7D99999999999   A!       s   0 &AAzshared (legacy)u2   ✅ Native attachment delivery is **active** for `z`.
Token: `z(`
Send `/setup-files revoke` to disable.Tu  🔧 Native attachment delivery is **not configured**.
**Step 1 (one-time, on the host):** create OAuth client credentials at https://console.cloud.google.com/apis/credentials → *Create credentials* → *OAuth client ID* → *Desktop app*. Download the JSON. Then on the host run:
```
python -m plugins.platforms.google_chat.oauth --client-secret /path/to/client_secret.json
```
**Step 2:** come back here and send `/setup-files start`.uf   🔧 Client credentials are stored but you haven't authorized yet. Send `/setup-files start` to begin.startuf   ⚠️ No client credentials stored on the host. Send `/setup-files` (no args) for setup instructions.r   ug   ❌ Couldn't generate the OAuth URL. Check the gateway logs and verify the client_secret.json is valid.z*[GoogleChat] /setup-files start failed: %su   ❌ Error: z01. Open this URL in your browser and authorize:
u>  

2. After clicking *Allow*, your browser will fail to load `http://localhost:1/?...&code=...`. That's expected.

3. Copy the entire failed URL from the browser's URL bar and paste it back here as: `/setup-files <PASTE_URL>` (or just the `code=...` value).

Tip: the URL contains your access grant — keep it private.revokezRevoked.z4Revoke completed (some steps may have been skipped).z+[GoogleChat] /setup-files revoke failed: %su   ❌ Error revoking: u   ✅ Done.
```
z
```u   ❌ Token exchange failed. The code may have expired or the URL is malformed. Send `/setup-files start` to get a fresh OAuth URL.z-[GoogleChat] /setup-files exchange failed: %sc                 .                                    S r  )r?  )	new_credsoauth_helpers   r:   r   z?GoogleChatAdapter._handle_setup_files_command.<locals>.<lambda>x  s    L@@KK r<   uZ   ✅ Authorized! Native attachment delivery is now active. Try asking me to send you a PDF.z0[GoogleChat] post-exchange creds load failed: %suz   ⚠️ Token exchanged but the gateway couldn't load the new credentials in-memory. Restart the gateway and the token at `z(` will be picked up.
Helper output:
```
)r9   r7   r)   ro   )rE   rN  rs   r8   r*  rO  _client_secret_pathrq   _token_pathr>  io
contextlibStringIOredirect_stdoutr  r+  get_auth_urlgetvalue
splitlines
SystemExitrN   rw   rx   r  r   popr   r   r   exchange_auth_code)rm   r   r  r  r  
sender_keypartsargr  client_secret_present
token_pathtoken_presentcredswhor  r  bufauth_urlr'   outputnew_apir  r  s   ```                  @@r:   r  z-GoogleChatAdapter._handle_setup_files_command  s
     : 	,+++++
 6BK\''))//111t
**"%e**q..eAhnnb
	 
	 
	 
	 
	 
	 
	 
	  &	002299;; " &11*==J&--//M !+22:>>>&*    5$5f== =)= = =         t( f
P         t&F         4'>>3355<<>> fG         t 			!!!!kkmm//44  !+$1:                       <<>>//11<<>>rB   fG         tt   @#   f0300111111111ttttt &OO O O	 	 	 	 	 	 	 	 	 4(??			!!!!kkmm//44 M M!+L,?LLLLLLLLLM M M M M M M M M M M M M M M--//=: P P PO   A3   f9C99:::::::::ttttt  +)--j$???,00TBBBB)-&&*#&9F999:::::::::4	III++--C++C00  ' 3S*                       \\^^))++FF 	 	 	&%        
 44 	 	 	NN?   &,s,,---------44444		%/2J       I $ ' 1KKKKK! !        2<ED-j9?FD0<<-6D**1D'f?         t %  	 	 	NNBC       	
 f7++J777 7 *07 7 7
 
 	
 	
 	
 	
 	
 	
 	
 ts   81H  )!G
H  GH  GAH   J>	J/I<<J#1L5 !L5L5 LL5 L	+L5 5N	N/NN(1Q9 "Q;Q9 QQ9 Q)Q9 9S	S /SSA6U 
V!VVOptional[MessageEvent]c           	       K   |                     d          p|                     d          pi }|                     d          pd}|                     d          p|                     d          pd                                }|                     d          pi }|                     d          pd}|                     d          pi }|                     d          pd}	|                     d	          p|                     d
          p|	}
|                     d
          pd}|r0|r.|                                                                | j        |<   |dv rdnd}|                     d          p|                     d          pd}|                                }|                     d          pi }t          |          }|rUt          |                     d          pd          }|r/|                    d          sd| d|                                 }g }g }t          j	        }|                     d          pg }|D ]}	 | 
                    |           d{V \  }}n*# t          $ r t                              d           Y Hw xY w|sO|                    |           |                    |pd           |t          j	        k    r|st          |pd          }|rt          j        }d}|r|r| j                            ||          }|dk    r;|dk    }|r|nd}|r|r|r|| j        |<   n.|r| j                            |d           n|}|r|r
|| j        |<   |                     ||                     d	          p|                     d          pd||p|	|
||	pd          }t-          |||||                     d          pd||          S )z4Parse a Chat API message into a hermes MessageEvent.r.  r&  rE   r$  r  r  Nr  r  r  DIRECT_MESSAGEDMdmr   r  r9   slashCommand	commandId/z/cmd_ 
attachmentz'[GoogleChat] attachment download failedapplication/octet-streamr   )r   	chat_name	chat_typeuser_id	user_namer  r  )r9   message_typer  raw_message
message_id
media_urlsmedia_types)r   upperrs   r8   r   r*   r7   r_   r   TEXT_download_attachmentrN   rw   r   r(  rc   COMMANDr   r   r   r  build_sourcer   )rm   rX  r  r.  r  r  r  r   r  sender_namer  r  r  r9   slashis_slash
command_idr   r  r  attachmentsatt
local_pathrY   prev_thread_countis_side_threadsession_thread_idr  s                               r:   r  z&GoogleChatAdapter._build_message_event  s      W%%?)9)9?RYYv&&,"
ii''G599[+A+AGRNNPP
""(bjj((0D""(bjj((.BM22Xfjj6I6IX[zz'**0b  	QJ 	Q4@4F4F4H4H4N4N4P4PD%j1&*BBBDD	ww~&&?#''&//?Rzz|| ''-2;; 	;UYY{339r::J ;$//#"6"6 ;2z22D2288:: !#
!#"'ggl++1r 	B 	BC)-)B)B3)G)G#G#G#G#G#G#G 
DD     !JKKK  j)))tA'ABBB{////5djbAA 	/&.L  	: 	 $ 8 = =K! !* .2N/= G4  @z @n @8C)*55 @)--j$??? + Dz D8C)*5""ii..I%))F2C2CIr "0[$'$,! # 
 
$ %wwv.$!#
 
 
 	
s   I%%$JJr  #Tuple[Optional[str], Optional[str]]c           	     v   K   |                     d          pd}|                     d          pd}|                     d          pd}|                     d          pi }|                     d          pd|                     d          pd|dk    r st                              d	           d
|fS d
}rqd fd}	 t          j        |           d
{V }nN# t
          $ rA}t                              dt          t          |                               d
}Y d
}~nd
}~ww xY w|rt                    st                              d           d
|fS d fd}		 t          j        |	           d
{V }nP# t          $ rC}t                              dt          t          |                               d
|fcY d
}~S d
}~ww xY w|d
|fS |r|                    d          d         nd}
d|
v r2d|
                    dd          d                                         z   }nd}|                    d          rt          ||pd          }nb|                    d          rt!          ||pd          }n9|                    d          rt#          ||pd          }nt%          ||
          }||fS )u#  Download an inbound attachment to the local cache; return (path, mime).

        Priority for bot Service Accounts:

          1. ``attachmentDataRef.resourceName`` via ``chat.media.download`` —
             the supported bot path. The Service Account bearer token has
             ``chat.bot`` scope which the Chat API authorises against the
             space membership.
          2. Drive-hosted files (``source == 'DRIVE_FILE'``) require user
             OAuth and Drive scope; skip with a log.
          3. Direct HTTP fetch of ``downloadUri`` only as a last resort —
             that URL is meant for user OAuth tokens (chat.google.com
             returns 401 for SA bearer tokens) and is unlikely to work,
             but we keep the path for forward-compat with Google changes.
        contentTyperE   r  r&  attachmentDataRefresourceNamedownloadUri
DRIVE_FILEzb[GoogleChat] Skipping Drive-picker attachment (no resourceName, would need user-OAuth Drive scope)Nr)   bytesc                    j                                                                       } ddlm} dd l}|                                } |||           }d}|s|                                \  }}||                                S )N)r  r   )MediaIoBaseDownloadF)	r   mediadownload_mediagoogleapiclient.httpr  r  BytesIO
next_chunkr  )	reqr  r  r  
downloaderr`  _statusresource_namerm   s	          r:   _fetch_mediaz<GoogleChatAdapter._download_attachment.<locals>._fetch_media=  s    n**,,;;!. <   EDDDDD			jjll00c::
 <$.$9$9$;$;MGT  <||~~%r<   z,[GoogleChat] media.download_media failed: %sz8[GoogleChat] Rejecting attachment fetch: non-Google hostc                     dd l mc mc m}  |                     j                  }|                    d          }|                                 |j        S )Nr      r_  )	google.auth.transport.requestsr   	transportrequestsAuthorizedSessionr   r   raise_for_statuscontent)garauthed_sessionr,   download_urirm   s      r:   
_fetch_uriz:GoogleChatAdapter._download_attachment.<locals>._fetch_uri\  so    <<<<<<<<<<<<!$!6!6t7H!I!I%)),)CC%%'''|#r<   zx[GoogleChat] downloadUri fetch failed (SA tokens often lack access here; this is expected for user-uploaded content): %sr  r  r  rG   r   r[   z.jpg)extr\   z.oggr]   z.mp4)r)   r  )r   rw   r   r  r+  r   rx   rX   r7   rT   rN   r*  rsplitr8   r_   r   r   r   r   )rm   r  rY   r  r&  attachment_data_refr}   r$  r'   r0  filenamer1  localr/  r#  s   `            @@r:   r  z&GoogleChatAdapter._download_attachment  s}     $ ~~m,,2))/R~~f%%+(nn-@AAGR+//??E2!~~m44: \!!-!KKC   : $  	& & & & & & &$.|<<<<<<<<   B%c#hh//    <L<(66 "N   Tz!$ $ $ $ $ $ $	"$.z:::::::: " " "# &c#hh//	   Tz!!!!!!" <: +/@4::c??2&&L(??Q//399;;;CCC??8$$ 	>*4S]FCCCEE__X&& 	>*4S]FCCCEE__X&& 	>*4S]FCCCEE-dH==Ed{s6   C 
D('7D##D($E? ?
G	8GGGr,  reply_tometadataOptional[Dict[str, Any]]r   c           	     $  K   |                      |||          }|                     |           	 |                     |                     |                    }|s&t	          dd          |                     |           S d}| j                            |d          }|t          k    rd}d}	t          |          D ]\  }
}d|i}|r|
dk    s|sd|i|d	<   	 |
dk    r!|r| 
                    ||           d{V }d
}	n|                     ||           d{V }|}b# t          $ r|}t          t          |dd          dd          }|dk    rR|                     ddd           t	          dt          |                    cY d}~c |                     |           S |dk    r|
dk    rB|r@t                               d           d}|                     ||           d{V }|}Y d}~3t                               d           t	          dd          cY d}~c |                     |           S |dk    rc| j                            |d          dz   | j        |<   | j        |         t(          k    r&t                               d| j        |                      d}~ww xY w|&t	          dd          |                     |           S |	rt          | j        |<   ||                     |           S # |                     |           w xY w)u  Send a text message.

        Signature matches ``BasePlatformAdapter.send``: ``content`` is the
        message body, ``reply_to`` is an optional message_id (the inbound
        message to thread under), and ``metadata`` may carry ``thread_id``
        (the resolved Google Chat ``spaces/X/threads/Y`` resource name).

        If a typing card is tracked for this chat, transform it in-place via
        ``messages.patch`` — NO delete+create. Google Chat shows a tombstone
        ("Message deleted by its author") on delete, which is visual noise.
        Patch rewrites the text of the existing message seamlessly.

        Also pauses the base class's ``_keep_typing`` loop for this chat so
        it can't post a racing typing card between the patch and the reply.

        If ``content`` exceeds MAX_MESSAGE_LENGTH, the first chunk patches
        the typing card (if any), subsequent chunks are new messages.
        r   Fzempty messagesuccessrM  Nr9   r   r&  r  Tr,   r-     chat_forbiddenz6Bot lacks access (removed from space or perms revoked)r2    z:[GoogleChat] Typing card disappeared; creating new messagez&[GoogleChat] send target 404; skippingztarget not foundr&   r   z8[GoogleChat] Rate limit hit %d times on chat; throttling)_resolve_thread_idpause_typing_for_chat_chunk_textformat_messager   resume_typing_for_chatr   r  _TYPING_CONSUMED_SENTINEL	enumerate_patch_messager  r   r3   rK  r7   rw   r   r   r   _RATE_LIMIT_WARN_THRESHOLDrx   )rm   r   r,  r6  r7  r  chunkslast_resulttyping_msg_namepatched_typingidxchunkr  r   r'   r-   s                   r:   sendzGoogleChatAdapter.send  s     2 ++Hh+PP	""7+++I	1
 %%d&9&9'&B&BCCF H!%GGGD ''0000A 15K"377FFO";;;"&"N'// - -
U(. 9#'''&,i%8DN(axxOx'+':':?D'Q'Q!Q!Q!Q!Q!Q!Q)-'+';';GT'J'J!J!J!J!J!J!J"(KK  ! ! !$WS&$%?%?4PPF}}--!1$\&+ .   
  *%s3xxHHHHHHHHHHJ ''0000I }} !888"KK \   /3O+/+?+?+N+N%N%N%N%N%N%NF*0K$HHHH$LMMM)%?QRRRRRRRRRR. ''0000- }} 155gqAAAE -g6  09=WWW"NN Z $ 5g >   C!D "!%GGG ''0000	  K1J%g.''0000D''0000sp   :K8 AK8 ADK8 J%)AJ J%K8 !AJ )K8 /*J J%K8 6A*J  J%%K8 K8 8LF)finalizer  rP  c          	     x  K   |st          dd          S t          |          t          k    r|dt          dz
           dz   }	 |                     |d|i           d{V S # t          $ r}t          t          |dd          d	d          }|d
k    r&| j                            |d          dz   | j        |<   t          dt          t          |                              cY d}~S d}~wt          $ rD}t                              dd           t          dt          |                    cY d}~S d}~ww xY w)uJ  Edit a previously sent message via ``messages.patch``.

        Required for the gateway tool-progress + token-streaming pipeline:
        ``GatewayStreamConsumer`` and ``send_progress_messages`` both gate
        on this method being overridden (see gateway/run.py:10199 and
        gateway/stream_consumer.py). Without it, Google Chat shows no
        tool activity (no "🔍 web_search…", no progressive token edits).

        ``message_id`` is the Google Chat resource name
        ``spaces/X/messages/Y``. ``finalize`` is unused here — Google
        Chat's patch API has no streaming lifecycle state, so the same
        patch closes the stream and any prior edit.

        404 (message gone) and 403 (perms revoked) are reported as
        non-success; the gateway falls back to ``send()`` for the next
        edit cycle.
        Fzmissing message_idr;  Nr   u   …r9   r,   r-   r&   r   z [GoogleChat] edit_message failedTr  )r   rO  _MAX_TEXT_LENGTHrG  r   r3   r   r   rX   r7   rN   rw   r  )rm   r   r  r,  rP  r'   r-   s          r:   edit_messagezGoogleChatAdapter.edit_message  s     2  	Ie3GHHHHw<<***4 01 445=G	=,,Z&'9JKKKKKKKKK 	 	 	WS&$774HHF}})--gq99A= %g. %6s3xx%@%@          	= 	= 	=LL;dLKKKe3s88<<<<<<<<<	=s1   A" "
D9,A6C("D9(D959D4.D94D9c           	        K   sdS d fd}	 t          j        |           d{V  dS # t          $ rj}t          t          |dd          dd          }|d	v rY d}~dS t                              d
t          t          |                               Y d}~dS d}~wt          $ r  t                              dd           Y dS w xY w)u  Delete a message — used sparingly (deletion creates a tombstone).

        The base contract returns False on unsupported. We do support it,
        but most internal code should prefer ``edit_message`` to avoid the
        "Message deleted by its author" tombstone. Provided so the
        gateway's stream-consumer fallback paths (e.g. removing an aborted
        partial preview) work correctly when explicit deletion is the
        right call.
        Fr)   ro   c                     j                                                                                                                                                                       d S N)r&  r  )r   r  messagesdeleter   r!  )r  rm   s   r:   
_do_deletez4GoogleChatAdapter.delete_message.<locals>._do_delete&  sP    %%''Z((d335566666r<   NTr,   r-   )r=  r?  z&[GoogleChat] delete_message failed: %sz"[GoogleChat] delete_message failedr  r   )	r  r+  r   r3   rw   r  rX   r7   rN   )rm   r   r  rY  r'   r-   s   ` `   r:   delete_messagez GoogleChatAdapter.delete_message  s$       	5	 	 	 	 	 	 		#J/////////4 	 	 	WS&$774HHF##uuuuuLL8!#c((++   55555 	 	 	LL=LMMM55	s    - 
C	$B!5B)C	C	r  r  c                p   K   g }d|v r|                     d           d|v r|                     d           d                    |          pdd |                                D             d fd}t          j        |           d{V }t          d	|                    d
                    S )z8Update a message's text (and optionally cards) in-place.r9   cardsV2r   c                "    i | ]\  }}|d v	||S ))r  r?   )rI   kvs      r:   
<dictcomp>z4GoogleChatAdapter._patch_message.<locals>.<dictcomp>J  s(    LLLtq!q7K7Ka7K7K7Kr<   r)   r  c                     j                                                                                                                                                                      S )N)r&  
updateMaskr  r  )r   r  rW  patchr   r!  )r  
patch_bodyrm   update_masks   r:   	_do_patchz3GoogleChatAdapter._patch_message.<locals>._do_patchL  sP    %%''L[zRRd335566	r<   NTr&  r<  r  r)   r  )r(  rP  r{   r  r+  r   r   )rm   r  r  update_mask_fieldsrf  r,   rd  re  s   ``    @@r:   rG  z GoogleChatAdapter._patch_message>  s        T>>%%f---%%i000hh122<f MLtzz||LLL
	 	 	 	 	 	 	 	 	 &y11111111$488FL3Q3QRRRRr<   r9   	List[str]c                   |sg S t          |          t          k    r|gS g }|}|rt          |          t          k    r|                    |           nl|                    ddt                    }|t          dz  k     rt          }|                    |d |                    ||d                                          }||S )N
r   rq  )rO  rR  r(  rfindr   )rm   r9   rI  	remainingcuts        r:   rB  zGoogleChatAdapter._chunk_textW  s     	It99(((6M	 		19~~!111i(((//$+;<<C%***&MM)DSD/***!#$$..00I  		1 r<   u'   [​‌‍‎‏⁠﻿︀-️󠄀-󠇯]c                6   |s|S |}i dgdfdt          j        dfd|          }t          j        dfd	|          }t          j        d
fd|t           j                  }t          j        dfd|          }t          j        dfd|          }t          j        dfd|          }| j                            d|          }t          j        dd|          }                                D ]\  }}|                    ||          }|S )a  Convert standard Markdown to Google Chat's formatting dialect.

        Google Chat renders a small subset: ``*bold*``, ``_italic_``,
        ``~strikethrough~``, fenced/inline code. Standard Markdown
        constructs (``**bold**``, ``# headers``, ``[text](url)``) do
        not render and need conversion before they reach Chat.

        Code blocks (fenced AND inline) are protected from transformation
        via placeholder substitution so backticks-wrapped content with
        literal asterisks or brackets stays intact. Invisible Unicode
        codepoints that render as tofu in Chat's restricted font stack
        are stripped at the end. Empty/None input passes through.

        Pattern lifted from PR #14965.
        r   valuer7   r)   c                J    dd          d}dxx         dz  cc<   | |<   |S )Nz GCr    r   r?   )rq  keycounterplaceholderss     r:   _phz-GoogleChatAdapter.format_message.<locals>._ph  s;    +71:+++CAJJJ!OJJJ %LJr<   z(```(?:[^\n]*\n)?[\s\S]*?```)c                @     |                      d                    S Nr   r   mrw  s    r:   r   z2GoogleChatAdapter.format_message.<locals>.<lambda>  s    cc!''!**oo r<   z	(`[^`]+`)c                @     |                      d                    S ry  rz  r{  s    r:   r   z2GoogleChatAdapter.format_message.<locals>.<lambda>  s    cc!''!**oo r<   z^#{1,6}\s+(.+)$c                l     d|                      d                                           d          S N*r   )r   rs   r{  s    r:   r   z2GoogleChatAdapter.format_message.<locals>.<lambda>  s3    cc3aggajj..0033344 r<   )flagsz\*\*\*(.+?)\*\*\*c                H     d|                      d           d          S )Nz*_r   z_*rz  r{  s    r:   r   z2GoogleChatAdapter.format_message.<locals>.<lambda>  s'    cc-qwwqzz---.. r<   z\*\*(.+?)\*\*c                H     d|                      d           d          S r  rz  r{  s    r:   r   z2GoogleChatAdapter.format_message.<locals>.<lambda>  s'    cc+aggajj+++,, r<   z\[([^\]]+)\]\(([^)]+)\)c                t     d|                      d           d|                      d           d          S )N<rq  |r   >rz  r{  s    r:   r   z2GoogleChatAdapter.format_message.<locals>.<lambda>  s9    cc8aggajj881771::88899 r<   rE   z  +r  )rq  r7   r)   r7   )rV   rW   	MULTILINE_INVISIBLE_REr{   r   )clsr,  r9   rt  rq  rw  ru  rv  s        @@@r:   rC  z GoogleChatAdapter.format_message  s   "  	N')#	 	 	 	 	 	 	 v,%%%%
 

 vl$=$=$=$=tDD v4444,	
 
 
 v ....
 
 v,,,,
 
 v&9999
 
  $$R.. vfc4(( ',,.. 	, 	,JC<<U++DDr<   c                    |r-dD ]*}|                     |          }|rt          |          c S +|r
d|v rd|vr|S |r| j                             |          }|r|S dS )uP  Return the Google Chat thread resource name to reply under, or None.

        Priority:
          1. ``metadata['thread_id']`` — populated by the gateway's session
             plumbing from ``SessionSource.thread_id`` (the inbound
             ``thread.name``). Canonical path for groups.
          2. ``metadata['thread_name']`` / ``metadata['thread_ts']`` — Slack
             precedent aliases that the broader codebase sometimes passes.
          3. ``reply_to`` if it already looks like a thread resource name
             (``spaces/X/threads/Y``). Message names ``spaces/X/messages/Y``
             cannot be converted to threads without an extra API call.
          4. ``self._last_inbound_thread[chat_id]`` — Google Chat DMs spawn
             a new thread per top-level user message, and the adapter
             intentionally drops thread_id from the source so the session
             key stays stable. Without this fallback, DM replies would
             land at top-level (a fresh thread separate from the user's),
             visually disconnected from the user's question.
        )r  r   	thread_tsz	/threads/z
/messages/N)r   r7   r   )rm   r6  r7  r   rt  rq  cacheds          r:   r@  z$GoogleChatAdapter._resolve_thread_id  s    0  	&@ & & S)) &u::%%%& 	x//L4P4PO 	.227;;F tr<   c                T    t          | j        t          j        d                    S )a>  Return a fresh AuthorizedHttp.

        googleapiclient's discovery client is NOT thread-safe because httplib2
        shares SSL state between calls. Passing a fresh http= to each
        ``execute()`` avoids record-layer failures when calls run in
        ``asyncio.to_thread`` workers. Cheap (~no network).
        r&  r_  r  )r   r   httplib2Httprg  s    r:   r!  z"GoogleChatAdapter._new_authed_http  s'     d/hmB6O6O6OPPPPr<   zchat-api-callop_namesync_fnCallable[[], Any]r  c                 K   t           }d}t          dt          dz             D ](}	 t          j        |           d{V c S # t          j        $ r  t          $ r}|}t          |          }|r|t          k    r |t          z  t          j	                    z  }t          ||z   t          t          z             }	t                              d||t          t          t          |                    |	           	 t          j        |	           d{V  n# t          j        $ r  w xY wt          |dz  t                    }Y d}~"d}~ww xY w||t#          | d          )u  Run ``sync_fn`` in a thread with bounded retry + jittered backoff.

        Wraps a sync Chat API call (typically a ``.execute()``) so transient
        429/5xx/timeout failures don't drop user-visible messages. Permanent
        failures (auth, client errors, validation) bubble up on the first
        attempt — see :func:`_is_retryable_error`. Cancellation propagates
        immediately, no extra retries after a CancelledError.

        Pattern lifted from PR #14965.
        Nr   z<[GoogleChat] %s attempt %d/%d failed (%s); retrying in %.2fsrq  z": retry loop exited without result)_RETRY_BASE_DELAYrange_RETRY_MAX_ATTEMPTSr  r+  rc  rN   r;   _RETRY_JITTERr{  rx  _RETRY_MAX_DELAYrw   rx   rX   r7   r}  r  )
rm   r  r  r  last_excr~  r'   r5  jitterwaits
             r:   _call_with_retryz"GoogleChatAdapter._call_with_retry  s       ",0Q 3a 788 	9 	9G9$.w77777777777)    9 9 9/44	  G/B$B$B.@56>+;m+KLL(W&9%c#hh//	  !------------   EAI'788#9& NgIIIJJJs0   AEBE9DED%%EEc                   K   ||d|                     d          pi }|                     d          rdd<   d fd}                     |d	
           d{V }|                     d          pi                      d          pd}|rK|rI	  j                            ||           n,# t          $ r t
                              dd           Y nw xY wt          d|                     d                    S )u1  POST spaces/{space}/messages via REST, returning SendResult.

        When ``body`` carries ``thread.name``, we MUST pass
        ``messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD`` —
        otherwise Google Chat silently ignores ``thread.name`` and
        creates a new thread anyway. From the official docs:

            "Default. Starts a new thread. Using this option ignores
             any thread ID or threadKey that's included."

        See https://developers.google.com/workspace/chat/api/reference/rest/v1/spaces.messages/create
        r   r  r  r&  $REPLY_MESSAGE_FALLBACK_TO_NEW_THREADmessageReplyOptionr)   r  c                      j                                                                         j        di                                                                S )Nr  r?   )r   r  rW  creater   r!  )kwargsrm   s   r:   
_do_createz5GoogleChatAdapter._create_message.<locals>._do_createE  s]    %%''" " " " d335566	r<   zmessages.creater  NrE   .[GoogleChat] outbound thread-count incr failedTr  rg  rh  )r   r  r   r   rN   rw   r  r   )rm   r   r  thread_metar  r,   resp_threadr  s   `      @r:   r  z!GoogleChatAdapter._create_message,  sb      -4T!B!Bhhx((.B??6"" 	R ,RF'(	 	 	 	 	 	 	 **:?P*QQQQQQQQ xx))/R44V<<B 	{ 	(--g{CCCC   D!      
 $488F3C3CDDDDs   B, ,&CCc                   K    j         v rdS  j        v rY	 t          j         j                                                 d           d{V  n# t          j        t          f$ r Y nw xY wdS                      d|          }ddi|rd|id<   t          j                     j        <   d fd}t          j	         |                      }	 t          j
        |           d{V  dS # t          j        $ r  w xY w)u  Post a visible 'Hermes is thinking…' marker message.

        NOT ephemeral (Google Chat has no ephemeral text messages outside
        slash command responses). ``send()`` PATCHes this marker in-place
        with the real response (no deletion tombstone). The typing card is
        either patched by ``send()`` (success) or by
        ``on_processing_complete`` (failure / cancellation).

        IMPORTANT — must place the typing card in the user's thread:
        ``messages.patch`` cannot change a message's ``thread`` (it's
        immutable on update). If we create the typing card at top-level
        and the user is replying inside thread T, send() will patch the
        top-level card in place — leaving the bot's whole response
        stranded outside the user's thread. We resolve the thread the
        same way send() does.

        IMPORTANT — cancellation safety:
        ``base.py``'s ``_keep_typing`` calls this through
        ``asyncio.wait_for(send_typing, timeout=1.5)``. When the
        create-API call takes longer than 1.5s, ``wait_for`` cancels
        ``send_typing`` mid-flight — but the underlying ``asyncio.to_thread``
        keeps running and creates a card in Chat that we have NO way to
        track (the storage line never runs). Next ``_keep_typing`` tick
        sees an empty slot and creates a SECOND card. Result: one orphan
        "Hermes is thinking…" stuck in chat forever, plus one card that
        gets patched into the reply.

        Fix: reserve the slot with an in-flight ``Event``, run the
        create in a background task, and ``await asyncio.shield`` it.
        Cancellation of THIS coroutine no longer cancels the create —
        the task runs to completion and the msg_id lands in the slot
        regardless.
        Nr^  r_  )r6  r7  r   r9   u   Hermes is thinking…r&  r  r)   ro   c                   K   	                                 d {V } | j        rS| j        rLj        vr| j        j        <   n3j                            g                               | j                   n,# t          $ r t          	                    dd           Y nw xY wj
                            d                                             d S # j
                            d                                             w xY w)Nz1[GoogleChat] send_typing background create failedTr  )r  r<  r  r   r   r   r(  rN   rw   r  r   r  set)r   r  r   	completedrm   s    r:   _create_and_recordz9GoogleChatAdapter.send_typing.<locals>._create_and_record  s;      #33GTBBBBBBBB> 4f&7 4 d&;;;9?9J-g66 4??#R  &!2333   G!       *..w=== *..w===s*   A6A< ;C <&B%"C $B%%C 1D
r   )r   r   r  rb  r  rd  KeyErrorr@  EventrU  shieldrc  )rm   r   r7  r  r  taskr  r  s   ``    @@r:   send_typingzGoogleChatAdapter.send_typing`  s     F d+++Fd000
&.w7<<>>           ((3   F++Hg , 
 
	 !'(?@ 	1$i0DNMOO	.7"7+	  	  	  	  	  	  	  	  	 4 "#5#5#7#788
	.&&&&&&&&&&&% 	 	 	 	s   9A A/.A/C8 8D	c                   K   | j                             |          }|sdS |t          k    r| j                             |d           dS dS )u}  Stop the typing indicator — NO-OP when a live card is tracked.

        Google Chat has no separate typing API: the "Hermes is thinking…"
        marker is a real message that ``send()`` patches in-place with the
        agent's reply. Deleting the marker creates a "Message deleted by
        its author" tombstone, which is visual noise.

        Upstream code (gateway/run.py and gateway/platforms/base.py) calls
        ``stop_typing`` at three moments per turn — typically BEFORE
        ``send()`` runs (so deleting the slot would leave ``send()``
        nothing to patch, forcing it to create a fresh message and leaving
        the original card as a tombstone). To fix this without modifying
        upstream contracts, ``stop_typing`` here is intentionally a NO-OP
        when the slot holds a real ``message_name``: the card is left in
        place so ``send()`` can patch it.

        Three cases:
          * Slot empty → nothing to do.
          * Slot holds SENTINEL → ``send()`` already patched the card;
            pop the sentinel so the next turn starts clean.
          * Slot holds a real ``message_name`` → leave it for ``send()``
            to consume. NO-OP.

        Stranded cards on error / cancellation paths (where ``send()``
        never runs) are reaped by ``on_processing_complete`` — see that
        hook for the patch-to-final-state cleanup.
        N)r   r   rE  r  )rm   r   currents      r:   stop_typingzGoogleChatAdapter.stop_typing  sZ      8 '++G44 	F///!%%gt444Fr<   r  r   outcomer   c                x  K   |j         dS |j         j        }	 | j                            |d          }|rk|t          k    r`|t
          j        k    rdnd}	 |                     |d|i           d{V  n,# t          $ r t          
                    dd           Y nw xY w| j                            |g           }|D ]N}	 |                     |ddi           d{V  "# t          $ r  t          
                    d	|d           Y Kw xY wdS # t          $ r  t          
                    d
d           Y dS w xY w)u  Reap typing card(s) after the message-handling cycle ends.

        SUCCESS: ``send()`` set the SENTINEL after patching. Pop it.

        FAILURE / CANCELLED: ``send()`` may not have run, leaving a real
        ``message_name`` in the slot. Patching the card to a final state
        (``"(interrupted)"``) avoids the tombstone that ``messages.delete``
        would create. If ``send()`` did run (e.g. base.py error-send branch
        patched it), the slot holds the SENTINEL — pop and exit.

        Orphan cards: when a background ``send_typing`` task creates a
        card AFTER ``send()`` already populated the slot (race window
        when the API call takes longer than _keep_typing's wait_for
        timeout), the orphan id is stashed in ``self._orphan_typing_messages``.
        Patch each orphan with an empty-ish marker so the user doesn't
        see "Hermes is thinking…" stuck forever.
        Nz(interrupted)z
(no reply)r9   z9[GoogleChat] on_processing_complete patch fallback failedTr     ·z0[GoogleChat] orphan typing-card patch failed: %sz5[GoogleChat] cleanup in on_processing_complete failed)r  r   r   r  rE  r   	CANCELLEDrG  rN   rw   r  r   )rm   r  r  r   r  labelorphans	orphan_ids           r:   on_processing_completez(GoogleChatAdapter.on_processing_complete  s     ( <F,& 	+//>>G 7&??? (/2C2M'M'MOO% --gGGGGGGGGGG    LLS!% !      266wCCG$  	--i&$HHHHHHHHHH    LLJ!D !        	 	 	LLGRV       	sY   <D A5 4D 5&BD B"D C D  'D
D 	D

D &D98D9Optional[SendResult]c                l  K   | j                             |          }|r|t          k    rdS | j                             |d           	 |                     |d|i           d{V }t          | j         |<   |S # t
          $ r2}t          t          |dd          dd          }|dk    rY d}~dS  d}~ww xY w)u  Patch the tracked typing card with ``text`` (no tombstone).

        Returns ``None`` if there's no real typing card to patch (caller
        should create a new message). Returns the patch result if the
        card was successfully patched. Raises on transient HttpErrors so
        the caller can decide whether to fall back to ``_create_message``.

        Leaves the SENTINEL in place when present: a previous ``send()``
        already consumed the typing card, and the SENTINEL must stay in
        the slot to keep the base class's ``_keep_typing`` loop from
        creating a fresh "Hermes is thinking…" card during any subsequent
        attachment send (which would later be reaped as "(no reply)").
        Nr9   r,   r-   r?  )r   r   rE  r  rG  r   r3   )rm   r   r9   r  r   r'   r-   s          r:   _consume_typing_card_with_textz0GoogleChatAdapter._consume_typing_card_with_text$	  s        '++G44 	'%>>>4!!'4000		..wGGGGGGGGF-FD!'*M 	 	 	WS&$774HHF}}ttttt	s   .A7 7
B3&B.-B..B3	image_urlcaptionc           	       K   |                      |||          }g }|r|                    |           |                    |           d                    |          }	 |                     ||           d{V }	|	|	S d|i}
|rd|i|
d<   |                     ||
           d{V S # t
          $ r5}t          dt          t          |                              cY d}~S d}~ww xY w)	u  Send an inline image via attachment URL (no upload).

        If a typing card is tracked for this chat, patch it in-place with
        the image (caption + URL) — same anti-tombstone pattern used by
        ``send()``. Otherwise create a new message.
        r:  rl  Nr9   r&  r  Fr;  )	r@  r(  rP  r  r  r   r   rX   r7   )rm   r   r  r  r6  r7  r  
text_partsr9   patchedr  r'   s               r:   
send_imagezGoogleChatAdapter.send_imageD	  s;      ++Hh+PP	 "
 	'g&&&)$$$yy$$		P ??NNNNNNNNG"$*D>D 5"()!4X--gt<<<<<<<<< 	P 	P 	Pe3DSXX3N3NOOOOOOOOO	Ps$   B( ?(B( (
C'2*C"C'"C'
image_pathr  c                   K   |                      |||d|                     ||                    d          |                     d {V S )Nzimage/*r7  r:  	mime_hintr  
_send_filer@  r   )rm   r   r  r  r6  r  s         r:   send_image_filez!GoogleChatAdapter.send_image_filed	  sp       __Z--h

:8N8NX_-`` % 
 
 
 
 
 
 
 
 	
r<   	file_path	file_namec                   K   |                      |||d |                     ||                    d          |          |           d {V S )Nr7  r:  )r  r  override_filenamer  )rm   r   r  r  r  r6  r  s          r:   send_documentzGoogleChatAdapter.send_documentr	  ss       __Y--h

:8N8NX_-``'	 % 
 
 
 
 
 
 
 
 	
r<   
audio_pathc                   K   |                      |||d|                     ||                    d          |                     d {V S )Nz	audio/oggr7  r:  r  r  )rm   r   r  r  r6  r  s         r:   
send_voicezGoogleChatAdapter.send_voice	  p       __Z!--h

:8N8NX_-`` % 
 
 
 
 
 
 
 
 	
r<   
video_pathc                   K   |                      |||d|                     ||                    d          |                     d {V S )Nz	video/mp4r7  r:  r  r  )rm   r   r  r  r6  r  s         r:   
send_videozGoogleChatAdapter.send_video	  r  r<   animation_urlc                F   K   |                      |||||           d{V S )zBGoogle Chat has no native animation type; fall back to send_image.)r  r6  r7  N)r  )rm   r   r  r  r6  r7  s         r:   send_animationz GoogleChatAdapter.send_animation	  sM       __]G % 
 
 
 
 
 
 
 
 	
r<   r'   r   c                4    t          |           pd}d|v pd|v S )u  Detect Google Chat's media.upload bot-auth rejection.

        Returns True for the canonical ``"doesn't support app
        authentication"`` wording (and the legacy
        ``ACCESS_TOKEN_SCOPE_INSUFFICIENT`` variant some older clients
        still see). Used to flag a misuse — calling ``media.upload``
        through the SA-authed Chat API client instead of the user-authed
        one. With correct routing this error should never fire in the
        adapter; it remains as a defensive check.
        rE   z"doesn't support app authenticationACCESS_TOKEN_SCOPE_INSUFFICIENT)r7   )r'   r9   s     r:   _is_app_auth_attachment_errorz/GoogleChatAdapter._is_app_auth_attachment_error	  s-     3xx~20D8 90D8	
r<   
__legacy__r  Optional[Any]c                  	K   ddl m}mm} | j                            |          }| j                            |          }||	 t          j        |||           d{V }n.# t          $ r! t                              dd           d}Y nw xY w|8| j                            |d           | j                            |d           dS || j        |<   |S 	 t          j        ||           d{V 		dS t          j        	fd           d{V }n.# t          $ r! t                              d|d           Y dS w xY w	| j        |<   || j        |<   |S )	a  Get (or build + cache) a user-authed Chat client for ``email``.

        Hits ``self._user_chat_api_by_email`` first; on miss, loads the
        per-user token from disk, refreshes if needed, builds an API
        client, and caches both. Refresh failures evict the slot so the
        next request goes back through the disk path (and ultimately the
        text-notice fallback if the user has revoked).
        r   )r>  r?  refresh_or_noneNz+[GoogleChat] cached per-user refresh raisedTr  c                                 S r  r?   )_buildr  s   r:   r   z;GoogleChatAdapter._load_per_user_chat_api.<locals>.<lambda>	  s    &&-- r<   z4[GoogleChat] per-user creds load/build failed for %s)rN  r>  r?  r  r   r   r   r  r+  rN   rw   r  r  )
rm   r  _load_refresh
cached_apicached_creds	refreshedapir  r  s
           @@r:   _load_per_user_chat_apiz)GoogleChatAdapter._load_per_user_chat_api	  s     	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 155e<<
044U;;!l&>!")"3HlE"R"RRRRRRR		 ! ! !AD     !				!
  ,00===)--eT:::t/8D%e,
	!+E599999999E}t)*?*?*?*?*?@@@@@@@@CC 	 	 	LLF     44	 ,1!%(.1$U+
s*   A% %(BBD 9D 'EE#Tuple[Optional[Any], Optional[str]]c                  K   |r!|                      |           d{V }|||fS | j        	 ddlm} t	          j        || j        d           d{V }n.# t          $ r! t          	                    dd           d}Y nw xY w|*t          
                    d           d| _        d| _        dS || _        | j        | j        fS dS )	uQ  Resolve the user-authed Chat client for an outbound attachment.

        Lookup order:
          1. Per-user token for ``sender_email`` — the asker's identity.
          2. Legacy single-user fallback (``self._user_chat_api``) for
             pre-multi-user installs.
          3. None — caller posts the setup-instructions text notice.

        Returns ``(client, identity_label)`` where ``identity_label`` is
        the sanitized email or the literal ``"__legacy__"`` sentinel.
        ``_invalidate_user_creds`` uses the label to evict the right slot
        on auth failure.
        Nr   )r  z([GoogleChat] legacy creds refresh raisedTr  uP   [GoogleChat] legacy user-OAuth refresh returned None — evicting fallback credsNN)r  r   rN  r  r  r+  r   rN   rw   r  rx   _LEGACY_USER_IDENTITY)rm   r  r  r  r  s        r:   _acquire_user_chat_apiz(GoogleChatAdapter._acquire_user_chat_api 
  sO        	)44\BBBBBBBBCL((*!      #*"3d4d# #      		  ! ! !>     !				!
  .   *.&&*#!z%.D"&(BBBzs   'A (B Bidentityc                    |sdS || j         k    rd| _        d| _        dS | j                            |d           | j                            |d           dS )u   Drop creds for ``identity`` after an auth failure.

        ``identity`` comes from ``_acquire_user_chat_api`` — either the
        sender email (per-user slot) or ``__legacy__`` for the fallback
        slot. None is a no-op.
        N)r  r   r   r   r  r   )rm   r  s     r:   _invalidate_user_credsz(GoogleChatAdapter._invalidate_user_creds/
  sl      	Ft111%)D""&DF!%%h555$((488888r<   rf   r  r  c           	     n  K   t           j                                      st          dd           S |p t           j                                      pd|pd| j                                      }|                     |           d{V \  } |                     ||           d{V S 	 | 	                    |pd           d{V  n,# t          $ r t                              d	d
           Y nw xY wd fd}		 t          j        |	           d{V }
n# t          $ r}t!          t!          |dd          dd          }|dv rVt                              d||           |                     |           |                     ||           d{V cY d}~S t          dt'          t)          |                              cY d}~S d}~ww xY w|
                    d          }|st          dd          S dd|igi}|r||d<   |rd|i|d<   |d|rdd<   d fd}	 t          j        |           d{V }|                    d          pi                     d          pd}rK|rI	 | j                            |           n,# t          $ r t                              dd
           Y nw xY wt          d
|                    d                    S # t          $ r5}t          dt'          t)          |                              cY d}~S d}~ww xY w)!u  Native Chat attachment via user-OAuth media.upload.

        Two-step on the wire: ``media.upload`` then
        ``spaces.messages.create`` with the returned ``attachmentDataRef``.
        BOTH calls go through a user-authed Chat API client — the
        SA-authed client is rejected by ``media.upload`` regardless of
        scopes.

        Multi-user routing: the bot looks up the most recent inbound
        sender for this ``chat_id`` and uses THAT user's stored OAuth
        token. Falls back to a legacy single-user token when present
        (for pre-multi-user installs), and to a setup-instructions text
        notice when neither is available.

        Google Chat ``messages.patch`` cannot add an attachment to an
        existing message, so we cannot transform the typing card directly
        into the file message. Instead we patch the typing card with the
        caption (or a single space when none) so it retires without a
        tombstone, then create the attachment message.
        Fzfile not found: r;  z
upload.binr  N)r   rf   r4  r  r  r  z4[GoogleChat] _send_file pre-patch typing-card failedTr  r)   r  c                     t          d          }                                                     di|                                           S )NF)mimetype	resumabler4  )r   r  
media_body)r   r  uploadr   )r  chat_apir   r4  rY   rf   s    r:   _uploadz-GoogleChatAdapter._send_file.<locals>._upload{
  sV    #D45IIIE  "$h/$   
 r<   r,   r-   )i  r=  u   [GoogleChat] media.upload auth failure for identity=%s (token revoked or scope missing) — falling back to text notice. Status=%sr  z$upload returned no attachmentDataRefr  r9   r&  r  r  r  r  c                                                                                        j        di                                 S )Nr?   )r  rW  r  r   )r  create_kwargss   r:   _create_with_attachmentz=GoogleChatAdapter._send_file.<locals>._create_with_attachment
  sG    !!) )') ) 	r<   rE   r  rg  rh  )r   rf   rq   r   basenamer   r   r  _post_attachment_fallbackr  rN   rw   r  r  r+  r   r3   rx   r  rX   r7   r   r   )rm   r   rf   r  r  r  r  r  r  r  upload_respr'   r-   attachment_refr  r   r,   r  r  r  r4  rY   s    ``               @@@@r:   r  zGoogleChatAdapter._send_file?
  s     : w~~d## 	Ne3Ld3L3LMMMM$N(8(8(>(>N,66044W==#'#>#>|#L#LLLLLLL( 77!# 8         	55gw~#NNNNNNNNNN 	 	 	LLF      	
	 
	 
	 
	 
	 
	 
	 
	 
	 
		 ' 1' : :::::::KK 	 	 	WS&$774HHF##-.6  
 ++H555!;;#%#' <               %6s3xx%@%@        !	( %)<== 	<    /@A 
  	#"DL 	1$i0DN 4;D(I(I 	6 ./	 	 	 	 	 	 		 *+BCCCCCCCCD  88H--388@@FBK ; ,11';GGGG    LLH!% !     
 &)9)9     	 	 	%6s3xx%@%@        	s   >C &DDD/ /
G(9A4G#-G(3*G#G(#G(>A
K5 	J% $K5 %&KK5 K&K5 5
L4?*L/)L4/L4r4  c                p  K   g }|r|                     |           |                    d| dddd| dg           dd                    |          i}|rd	|i|d
<   	 |                     ||           d{V  n,# t          $ r t
                              dd           Y nw xY wt          dd          S )aA  Post a text notice when native attachment delivery is unavailable.

        Tells the user that file delivery requires a one-time consent
        flow (``/setup-files``) and reports the local-host path so the
        file isn't lost. Returns ``success=False`` so callers know the
        attachment did not land.
        u   ⚠️ No he podido adjuntar **z**.u   Google Chat sólo permite adjuntar archivos cuando el bot tiene permiso explícito tuyo (OAuth de usuario). Es un consentimiento único que se hace desde este chat.uD   **Para activarlo:** envía `/setup-files` y sigue las instrucciones.u-   Mientras tanto el archivo está en el host: ``r9   rl  r&  r  Nz3[GoogleChat] attachment fallback notice send failedTr  FuO   google_chat: native attachment requires user OAuth — run /setup-files in chatr;  )r(  r)  rP  r  rN   rw   r  r   )rm   r   rf   r4  r  r  linesr  s           r:   r  z+GoogleChatAdapter._post_attachment_fallback
  s.       	"LL!!!;h;;;2 SCDCCC
 	 	 	 !'		%(8(89 	1$i0DN	&&w5555555555 	 	 	LLE      	
 '
 
 
 	
s   A; ;&B$#B$c           	        K   	 t          j         fd           d{V }nR# t          $ rE}t                              dt          t          |                               ddcY d}~S d}~ww xY w|                    d          p|                    d          pd                                }|                    d	          p}||d
v rdnddS )z)Return {name, type, chat_id} for a space.c                     j                                                                                                                                      S rV  )r   r  r   r   r!  )r   rm   s   r:   r   z1GoogleChatAdapter.get_chat_info.<locals>.<lambda>  sB    --//'""d335566 r<   Nz%[GoogleChat] get_chat_info failed: %sr   )r&  r$  r   r  r$  rE   r  r  r  )	r  r+  r   rw   r  rX   r7   r   r  )rm   r   r   r'   r  displays   ``    r:   get_chat_infozGoogleChatAdapter.get_chat_info  s;     
	J *7 7 7 7 7       DD
  	J 	J 	JLL79J3s889T9T   $WIIIIIIII		J
 hh{++Etxx/?/?E2LLNN
((=))4W&*BBBDD
 
 	
s   % 
A4:A/)A4/A4)r   r   )r)   r   )r)   r   )r   r   r)   ro   )r   r   r)   r*   )r   r   r)   ro   )r)   rg   )r)   r
  )r  r7   r)   ro   r)   r*   r   )rE   )r  r  r  r7   r)   r  )r4  r   r)   ro   )rX  r  r  r  r)   ro   r  )
r   r7   r  r
  r  r7   r  r
  r)   r*   )rX  r  r  r  r)   r  )r  r  r)   r  r  )
r   r7   r,  r7   r6  r
  r7  r8  r)   r   )
r   r7   r  r7   r,  r7   rP  r*   r)   r   )r   r7   r  r7   r)   r*   )r  r7   r  r  r)   r   )r9   r7   r)   rj  )r,  r7   r)   r7   )r6  r
  r7  r8  r   r
  r)   r
  )r  r  r  r7   r)   r   )r   r7   r  r  r)   r   )r   r7   r7  r   r)   ro   )r   r7   r)   ro   )r  r   r  r   r)   ro   )r   r7   r9   r7   r)   r  )NNN)r   r7   r  r7   r  r
  r6  r
  r7  r8  r)   r   )r   r7   r  r7   r  r
  r6  r
  r  r   r)   r   )r   r7   r  r7   r  r
  r  r
  r6  r
  r  r   r)   r   )r   r7   r  r7   r  r
  r6  r
  r  r   r)   r   )r   r7   r  r7   r  r
  r6  r
  r  r   r)   r   )r   r7   r  r7   r  r
  r6  r
  r7  r8  r)   r   )r'   r   r)   r*   )r  r7   r)   r  )r  r
  r)   r  )r  r
  r)   ro   )r   r7   rf   r7   r  r
  r  r
  r  r
  r  r
  r)   r   )r   r7   rf   r7   r4  r7   r  r
  r  r
  r)   r   )r   r7   r)   r  )>r   r   r   r   rR  MAX_MESSAGE_LENGTHrw  rz  ry  rn   r   r   staticmethodr   r   r  r	  r  r  r/  r\  rh  rV  r  ru  r  r  r  r  rO  rS  rZ  rG  rB  rV   compiler  classmethodrC  r@  r!  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  __classcell__)r   s   @r:   r   r   w  sv         *  SY SY SY SY SY SYpG G G GR( ( ( (> R R R \R Z Z Z \Z? ? ? ?$7 7 7 7
   	\ 	\ 	\ 	\% % % %TV V V Vp1 1 1 18E E E ET 13\ \ \ \ \\|   B$F $F $F $FV '+^ ^ ^ ^ ^@|
 |
 |
 |
|q q q qt #'-1d1 d1 d1 d1 d1X += += += += += +=Z# # # #JS S S S2   6 BJ		 M M M M [Mf "&	# # # # #JQ Q Q Q '	,K ,K ,K ,K ,K ,K\2E 2E 2E 2Eha a a a aF# # # #J7 7 7 7x   H "&"&-1P P P P PH "&"&
 
 
 
 
$ "&#'"&
 
 
 
 
( "&"&
 
 
 
 
$ "&"&
 
 
 
 
$ "&"&-1
 
 
 
 
< 
 
 
 \
" ). . . .`- - - -^9 9 9 9, $(+/T T T T Tl(
 (
 (
 (
Z
 
 
 
 
 
 
 
r<   r   r   r   c                    t          | di           pi }t          |                    d          o|                    d                    S )zPlugin-side config gate: require both Pub/Sub project and subscription.

    Mirrors the legacy dispatch entry in ``gateway/config.py`` so the
    registry can decide whether the platform is configured without
    importing the legacy table.
    r   r   r   )r3   r*   r   )r   r   s     r:   r   r     sM     FGR((.BE		,BEII.A$B$B  r<   c                     t                      sdS t          j        d          pt          j        d          } t          j        d          pt          j        d          }t          | o|          S )u	  ``check_fn`` for the platform registry pass — stricter than the
    deps-only ``check_google_chat_requirements``.

    The registry pass at ``gateway/config.py:_apply_env_overrides`` adds
    the platform to ``cfg.platforms`` whenever ``check_fn`` returns True.
    For backward compat with the pre-plugin behavior, we ALSO require
    the minimum Pub/Sub env vars so an unconfigured user doesn't
    accidentally see ``google_chat`` enabled. This matches the legacy
    ``if gc_project and gc_subscription`` gate.
    FGOOGLE_CHAT_PROJECT_IDGOOGLE_CLOUD_PROJECTGOOGLE_CHAT_SUBSCRIPTION_NAMEGOOGLE_CHAT_SUBSCRIPTION)r@   r   r   r*   )r   r   s     r:   _check_for_registryr  *  sz     *++ u
	*++ 	-9+,, 
 		122 	19/00  (L)))r<   c                \    t          t          | dd                    ot          |           S )z7``GatewayConfig.get_connected_platforms()`` polls this.enabledF)r*   r3   r   )r   s    r:   _is_connectedr  B  s*    	51122O7G7O7OOr<   r8  c                 |   t          j        d          pt          j        d          } t          j        d          pt          j        d          }| r|sdS | |d}t          j        d          pt          j        d          }|r||d	<   t          j        d
          }|r|t          j        dd          d|d<   |S )u  Seed ``PlatformConfig.extra`` from env vars during
    ``_apply_env_overrides``.

    The registry's env-enablement hook is called BEFORE the adapter is
    constructed, so ``gateway status`` and ``get_connected_platforms()``
    reflect env-only configuration without instantiating the Pub/Sub client.
    Returns ``None`` when the required Pub/Sub project/subscription aren't
    set; the caller then skips auto-enabling the platform.

    The special ``home_channel`` key in the returned dict is handled by the
    core hook — it becomes a proper ``HomeChannel`` dataclass on the
    ``PlatformConfig`` rather than being merged into ``extra``.
    r  r  r  r  N)r   r    GOOGLE_CHAT_SERVICE_ACCOUNT_JSONr   r   GOOGLE_CHAT_HOME_CHANNELGOOGLE_CHAT_HOME_CHANNEL_NAMEHome)r   r&  r'  )r   r   )r   r   seedsa_jsonr   s        r:   _env_enablementr#  G  s     		*++ 	-9+,, 
 		122 	19/00    t) D
 		455 	79566   /'.#$9/00D 
I=vFF 
  
^ Kr<   ro   c                    ddl m} m}m}m}m}m}m}  | d          }|r |d| d            |dd          sdS  |d	            |d
            |d            |d            |d            |d            |d            |d            |d            |d            |d            |d            |d            |d            |d            |d | d          pd          }|s |d           dS  |d|                                            |d | d          pd          }	|	s |d           dS  |d|	                                            |d | d          pdd           }
|
r |d|
                                            |d!d          rS |d" | d#          pd          }|r, |d#|	                    d$d                      |d%           n$ |d#d           n |d&d'            |d(            |d) | d*          pd          }|r |d*|                                           t                        |d+            |d,           dS )-a  Walk the user through Google Chat configuration via ``hermes setup``.

    The setup wizard at ``hermes_cli/gateway.py`` calls this for plugin
    platforms instead of using the in-tree ``_PLATFORMS`` data block. The
    flow mirrors the in-tree built-ins: print the GCP setup instructions,
    prompt for env vars, persist them to ``~/.hermes/.env`` so the next
    gateway restart picks them up.
    r   )get_env_valuesave_env_valuepromptprompt_yes_no
print_infoprint_successprint_warningr  z/Google Chat: already configured (subscription: )zReconfigure Google Chat?FNz@Google Chat needs a GCP project, a Pub/Sub topic + subscription,zBand a Service Account with Pub/Sub Subscriber on the subscription.zWalkthrough:zP  1. Create or select a GCP project; enable Google Chat API + Cloud Pub/Sub API.zA  2. Create a Service Account (no project-level IAM role needed).zN  3. Create a Pub/Sub topic (e.g. hermes-chat-events) and a Pull subscription.zU  4. On the TOPIC: add chat-api-push@system.gserviceaccount.com as Pub/Sub Publisher.zH  5. On the SUBSCRIPTION: grant your Service Account Pub/Sub Subscriber.z+  6. Download the Service Account JSON key.uK     7. Google Chat API console → Configuration: connection = Cloud Pub/Sub,zA     point at the topic, enable 1:1 + group, restrict visibility.zP  8. Install the bot in a space (fires ADDED_TO_SPACE and resolves its user_id).rE   z<Full guide: website/docs/user-guide/messaging/google_chat.mdz GCP project ID (e.g. my-project)r  )r   u5   Project ID is required — skipping Google Chat setupz:Pub/Sub subscription (projects/<proj>/subscriptions/<sub>)u7   Subscription is required — skipping Google Chat setupz-Path to Service Account JSON (or inline JSON)r  T)r   passwordz0Restrict access to specific users? (recommended)z%Allowed user emails (comma-separated)GOOGLE_CHAT_ALLOWED_USERSr  zAllowlist configuredGOOGLE_CHAT_ALLOW_ALL_USERStrueuA   ⚠️  Open access — anyone who can DM the bot can command it.zFHome space for cron/notification delivery (e.g. spaces/AAAA, or empty)r  z1Google Chat configuration saved to ~/.hermes/.envz+Restart the gateway: hermes gateway restart)hermes_cli.configr%  r&  r'  r(  r)  r*  r+  rs   r   print)r%  r&  r'  r(  r)  r*  r+  existing_subr   r   r   allowedr   s                r:   interactive_setupr5  r  s                     !=!@AAL 
T\TTTUUU}7?? 	FJQRRRJSTTTJ~JabbbJRSSSJ_```JfgggJYZZZJ<===J\]]]JRSSSJabbbJrNNNJMNNNJrNNNf*677=2  G  MNNNN+W]]__===6D=>>D"  L  OPPPN2L4F4F4H4HIIIf7@AAGR  G
  L97==??KKK}GNN [&3!M"=>>D"
 
 
  	<N6R8P8PQQQM01111N6;;;;4f===YZZZ6P899?R  D  A14::<<@@@	GGGMEFFFJ<=====r<   c                    |                      ddd t          t          t          g ddt          t
          dddd	d
dd           dS )uB  Plugin entry point — called by the Hermes plugin system at startup.

    Registers the Google Chat adapter under the ``google_chat`` name.
    The gateway's ``_create_adapter`` consults the platform registry
    BEFORE its built-in if/elif chain, so this registration is what
    drives adapter creation at runtime.
    r   zGoogle Chatc                     t          |           S r  )r   )cfgs    r:   r   zregister.<locals>.<lambda>  s    $5c$:$: r<   )r  r  r  z'pip install 'hermes-agent[google_chat]'r  r.  r/  r   u   💬Tu{  You are on Google Chat. Limited markdown subset is rendered: *bold*, _italic_, ~strike~, `code`. No headings or lists. Message size limit: 4000 characters; longer responses are split across multiple messages. You are in a space (DM or group). Images render inline; audio, video, and document attachments render as download cards (no native voice/video UI). To send files, include MEDIA:/absolute/path/to/file in your response. Native file attachments require the user to run /setup-files once in their own DM — until they do, file requests fall back to a text notice with the host path. Do NOT generate interactive Card v2 buttons — Google Chat interactivity is not yet supported by this gateway; ask for typed confirmations instead. While you are generating a response, a 'Hermes is thinking…' marker message appears in the space and is deleted once your response is ready. You do NOT have access to Google Chat-specific APIs — you cannot search space history, list space members, or manage spaces. Do not promise to perform these actions; explain that you can only read messages sent directly to you and respond in the same space/thread.)r&  r  adapter_factorycheck_fnvalidate_configis_connectedrequired_envinstall_hintsetup_fnenv_enablement_fncron_deliver_env_varallowed_users_envallow_all_envmax_message_lengthemojiallow_update_commandplatform_hintN)register_platformr  r   r  r5  r#  )ctxs    r:   registerrJ    sx     ::$("
 
 

 ?" * 853  !E  6 6 6 6 6r<   )r'   r(   r)   r*   r  )rA   r7   r)   r*   )r9   r7   r)   r7   )rY   r7   r)   r   )r   r   r)   r*   )r)   r8  r   )Rr   
__future__r   r  rt   loggingr   r{  rV   pathlibr   rg   typingr   r   r   r   r	   r
   r  google.cloudr   google.api_corer   rR  google.oauth2r   google_auth_httplib2r   googleapiclient.discoveryr   r;  googleapiclient.errorsr   r  r   r>   r   rN   gateway.configr   r   gateway.platforms.helpersr   gateway.platforms.baser   r   r   r   r   r   r   r   r   	getLoggerrw   r  r   r   rR  rH  r  r  r  r  	frozensetr6   r;   rE  r@   rR   rT   rX   rc   re   r   r   r  r  r#  r5  rJ  r?   r<   r:   <module>rZ     s  # #J # " " " " "    				  				 ! ! ! ! ! ! = = = = = = = = = = = = = = = =OOO&&&&&&<<<<<<------333333@@@@@@000000444444  	 	 	!HINONMIOOO	 4 3 3 3 3 3 3 3 	    9 9 9 9 9 9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
& 
	:	;	; #
A    /,        $9%>%>%>??    J ) ! ! ! !	 W W W W    6       "k k k k k k k k\^&
 ^&
 ^&
 ^&
 ^&
+ ^&
 ^&
 ^&
LM
 
 
 
* * * *0P P P P
( ( ( (VZ> Z> Z> Z>z> > > > > >s   0A) )BB