Google Chat Webhook Handler
Google Workspace integration for BetterFiles — receives events from Google Chat spaces, verifies JWT identity (only Google’s service accounts are accepted), and routes to command handlers, slash command handlers, and link preview logic. Mounted on the quo-handler-enhanced Express app via mountGchatRoutes(app).
Endpoint
| Path | Provider | Auth |
|---|---|---|
https://discord-bot.reri.co/webhook/gchat | Google Chat | JWT Bearer |
Port: Shares 18792 with quo-handler-enhanced.js (mounted via mountGchatRoutes(app))
Not a standalone service — routes added to the OpenPhone/Quo handler Express app.
Configure in Google Chat API:
- App Configuration → Connection settings → App URL:
https://discord-bot.reri.co/webhook/gchat
Auth method
Google JWT Bearer token — NO HMAC. Google sends a signed JWT in the Authorization: Bearer <token> header from its Chat service account.
Verification via OAuth2Client.verifyIdToken() from google-auth-library:
Audience: https://discord-bot.reri.co/webhook/gchat
Allowed issuers:
- chat@system.gserviceaccount.com
- service-874926066580@gcp-sa-gsuiteaddons.iam.gserviceaccount.com
Any request with a missing, expired, wrong-audience, or unauthorized-issuer token is rejected with 401. This is stronger than HMAC for external APIs — the token is cryptographically signed by Google’s private key.
Payload shape
Google Chat sends JSON event objects:
| Event type | Description |
|---|---|
ADDED_TO_SPACE | Bot added to a new Chat space |
REMOVED_FROM_SPACE | Bot removed from a space |
MESSAGE | User sends text or structured card message |
CARD_CLICKED | User interacts with an interactive card |
Key fields:
{
"type": "MESSAGE",
"space": { "name": "spaces/AAAA...", "type": "ROOM" },
"message": {
"text": "string",
"slashCommand": { "commandId": "number" }
},
"user": { "email": "string", "displayName": "string" }
}Downstream dispatch
gchat-handler.js (mounted on quo-handler-enhanced)
├─ verifyGoogleChatToken(req) → OAuth2Client.verifyIdToken() → 401 if invalid
├─ parseCommand(message) → gchat-commands.js
├─ handleCommand() → workspace-betterfiles/scripts/lib/gchat-commands.js
├─ handleSlashCommand() → slash command routing
├─ handleLinkPreview() → URL metadata expansion for link cards
└─ getChatClient() → workspace-betterfiles/scripts/lib/gchat-client.js
→ async message updates / card replacements
State store: workspace-betterfiles/data/gchat-state.json — persists space membership and session state.
Dedup strategy
State managed in gchat-state.json. ADDED_TO_SPACE / REMOVED_FROM_SPACE events are idempotent (upsert space record). MESSAGE events are not deduped — Google Chat guarantees at-most-once delivery for push events.
Audit trail
- Authentication failures: Any JWT verification failure (missing token, wrong audience, unauthorized issuer) is logged as a security exception before returning 401.
- Logging: Via parent
quo-handler-enhancedapplication logging middleware. - State persistence:
gchat-state.jsonserves as an implicit audit trail of space membership changes.
Related
- webhook-architecture — Cross-handler governance; gchat uses Google JWT Bearer (not HMAC) — do not attempt to add HMAC verification
- _summary — BetterFiles agent is the primary consumer; gchat-commands routes to BetterFiles workspace scripts
- cron-timer-registry — Handler shares port 18792 with openphone-webhook.service