Skip to content

feat(googlechat): Google Chat channel integration#148

Open
tuntran wants to merge 13 commits intonextlevelbuilder:mainfrom
tuntran:feat/googlechat-channel
Open

feat(googlechat): Google Chat channel integration#148
tuntran wants to merge 13 commits intonextlevelbuilder:mainfrom
tuntran:feat/googlechat-channel

Conversation

@tuntran
Copy link

@tuntran tuntran commented Mar 11, 2026

Summary

  • Add Google Chat channel with webhook endpoint, OIDC token verification, and emoji reactions
  • Store service account JSON content directly in DB instead of requiring file path on disk
  • Add google_chat to valid channel types allowlist
  • Wire GoogleChat factory into instance loader and config-based channel registration
  • UI fields for GoogleChat channel setup with SA JSON textarea

Convention Fixes (vs PR #135)

  • Add default GroupPolicy "pairing" for DB instances (security parity with Discord/Feishu/Slack)
  • Fix BaseChannel name to use channels.TypeGoogleChat constant ("google_chat")
  • Warn at startup when OIDC verification is disabled (no project_number)
  • Extract resolveServiceAccountJSON to credentials.go (channel.go under 200 lines)
  • Coolify deployment separated out — not included in this PR

Changes

  • internal/channels/googlechat/ — full channel implementation (webhook handler, bot API, factory, types, credentials)
  • internal/config/config_channels.go — add GoogleChatConfig struct with service_account_json field
  • internal/channels/channel.go — add TypeGoogleChat constant
  • cmd/gateway.go — register GoogleChat factory in instance loader
  • cmd/gateway_channels_setup.go — add GoogleChat to config-based channel registration
  • ui/web/src/pages/channels/ — UI fields for GoogleChat channel setup
  • internal/config/config_secrets.go — mark SA JSON as sensitive

Copy link
Contributor

@viettranx viettranx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Google Chat Channel Integration

Risk Level: Medium | Verdict: Request Changes

Overall the implementation follows existing channel patterns well. However, there are 2 critical security issues and several important items to address before merge.


Critical (must fix before merge)

C1. UI default policy is "open" instead of "pairing"
ui/web/src/pages/channels/channel-schemas.ts — The GoogleChat schema uses inline policy options with default "open" for both group_policy and dm_policy. All other channels use the shared groupPolicyOptions/dmPolicyOptions with default "pairing". This is a security regression — anyone in the Google Workspace can talk to the bot without approval.

Fix: Use the shared groupPolicyOptions/dmPolicyOptions arrays and set default to "pairing", consistent with all other channels.

C2. Config-based path missing default GroupPolicy "pairing"
cmd/gateway_channels_setup.go — The GoogleChat config registration doesn't set a default GroupPolicy: "pairing". When a user leaves the field empty, bot.go will default to "open".

Fix: Add GroupPolicy: "pairing" default in the config-based registration path, matching what Discord/Feishu/Slack do.


Important (should fix)

H1. Dedup goroutine leak potential
channel.go — Each incoming message spawns a new goroutine with go func() { time.Sleep(5*time.Minute); cache.Delete(key) }() for dedup cleanup. Under high traffic this can accumulate thousands of sleeping goroutines.

Fix: Use time.AfterFunc(5*time.Minute, func() { cache.Delete(key) }) instead — it uses the runtime timer heap without blocking a goroutine.

H2. ServiceAccountFile allows arbitrary file reads
credentials.goos.ReadFile(path) with no path validation. This is a potential path traversal vector. Since ServiceAccountJSON inline already works, consider removing the file-based path entirely, or at minimum validate the path is within an expected directory.

H3. Unsafe type assertion
channel.goval.(*reactionState) will panic if the sync.Map contains a value of unexpected type. Use the comma-ok pattern:

rs, ok := val.(*reactionState)
if !ok {
    return
}

Suggestions

M1. No test coverage — There are zero unit tests for this channel. Other channels like Slack have tests. At minimum, consider tests for extractSenderInfo, isBotMentioned, resolveServiceAccountJSON, and the message chunking logic.

M2. ReactionLevel not validated — Invalid values fall through silently. Consider logging a warning for unrecognized levels.

M3. io.ReadAll error ignored in api.go — The error from reading the response body should be checked.

M4. Trailing blank line at end of channel.go — minor cleanup.


Nice work on the overall structure — the modular split (api/bot/channel/handler/factory/types/credentials) is clean and follows the project's file-size guidelines well. Just need to nail the security defaults and the items above before merging. 👍

tuntran added a commit to tuntran/goclaw that referenced this pull request Mar 12, 2026
- Security: default dm_policy and group_policy to "pairing" instead of
  "open" in both UI schema and bot runtime fallback
- Replace goroutine+sleep dedup cleanup with time.AfterFunc to prevent
  goroutine leak
- Remove ServiceAccountFile path support, only keep inline JSON
- Use comma-ok type assertion in removeReaction to prevent panic
- Check io.ReadAll error in API client instead of discarding
- Warn on unrecognized reaction_level value
- Remove trailing blank line in channel.go
- Add unit tests for credentials, handler helpers, and dedup logic
@tuntran tuntran requested a review from viettranx March 12, 2026 03:19
@tuntran
Copy link
Author

tuntran commented Mar 12, 2026

All review feedback addressed in c29b075:

Critical

  • C1 ✅ UI defaults changed to "pairing", using shared
    dmPolicyOptions/groupPolicyOptions
  • C2 ✅ Runtime fallback in bot.go now defaults to "pairing". Note: checked
    Discord/Slack/Feishu — none set GroupPolicy at config registration level in
    gateway_channels_setup.go either; all rely on runtime fallback in bot.go. Kept consistent.

Important

  • H1 ✅ Replaced go func() { time.Sleep(); delete } with time.AfterFunc
  • H2 ✅ Removed ServiceAccountFile entirely — only inline JSON supported now
  • H3 ✅ Comma-ok type assertion in removeReaction

Suggestions

  • M1 ✅ Added unit tests: credentials_test.go, handler_test.go, bot_test.go
    (concurrent dedup, cleanup, helpers)
  • M2 ✅ slog.Warn on unrecognized reaction_level
  • M3 ✅ io.ReadAll error checked in api.go
  • M4 ✅ Trailing blank line removed

Ready for re-review.

tuntran added 3 commits March 13, 2026 03:32
…ad registration

Channels registered after Manager.Reload() were not getting webhook routes mounted,
causing 404 errors for their webhook handlers. Fixed by delegating webhook requests
through Manager's webhookMap instead of mounting static routes at startup.

- Add webhookMap tracking active webhook paths and their Channel handlers
- Add WebhookServeHTTP() to dynamically route webhook requests to registered channels
- Add MountNewWebhookRoutes() to mount handlers for newly registered channels
- Update Reload() to trigger webhook route mounting for new channels
- Add comprehensive unit tests for webhook mounting and delegation

Fixes: Google Chat Add-on returning 404 on /googlechat/events after channel reload
@tuntran tuntran force-pushed the feat/googlechat-channel branch from 019d018 to 6c3cec0 Compare March 12, 2026 20:32
Copy link
Contributor

@viettranx viettranx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — Google Chat Channel Integration

Verdict: APPROVE với một số yêu cầu sửa

Điểm tốt

  • Cấu trúc file tách rõ ràng, mỗi file dưới 200 dòng, follow đúng patterns của codebase
  • OIDC verification dùng đúng JWK endpoint riêng của Google Chat (chat@system.gserviceaccount.com) thay vì generic OAuth2 certs
  • Dynamic webhook mounting — refactor Manager hỗ trợ MountNewWebhookRoutes() để reload channel không cần restart
  • Security defaults tốt: GroupPolicy mặc định "pairing" cho DB instances, warn khi OIDC disabled, SA JSON được mask trong config_secrets.go
  • Tests có chất lượng: concurrent dedup, mention detection, sender extraction, dynamic route registration

Cần sửa trước merge

P0 — io.ReadAll không limit response body (api.go:93)

respBody, err := io.ReadAll(resp.Body)

Handler đã làm đúng (io.LimitReader(r.Body, 1<<20)), nhưng API client thì chưa. Nếu Google trả response lớn bất thường (lỗi, redirect) có thể gây OOM. Sửa:

respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1MB

P1 — senderID chứa display name (handler.go:128-141)

senderID = user.Name + "|" + displayName

Nếu user đổi display name → senderID thay đổi → session mới, mất context cũ. Các channel khác (Telegram, Discord) dùng ID thuần túy. Nên dùng user.Name làm senderID, truyền displayName riêng qua metadata (đã có metadata["sender_name"]).

P2 — Config comment sai default (config_channels.go:28-29)

DMPolicy  string `json:"dm_policy,omitempty"`   // "open" (default)
GroupPolicy string `json:"group_policy,omitempty"` // "open" (default)

Comment nói default "open" nhưng code trong bot.go:43-47factory.go:67-69 set default "pairing". Sửa comment cho khớp.


Nice-to-have (follow-up PR OK)

  1. isValidChannelType bị duplicate — Cùng hàm giống hệt ở internal/gateway/methods/channel_instances.gointernal/http/channel_instances.go. Nên refactor thành 1 hàm IsValidChannelType() trong internal/channels/channel.go dùng constants đã có.

  2. OIDC thiếu validate iat claim — Chỉ check exp, không check iat. Token với iat trong tương lai có thể là forged. Nên thêm:

    if claims.Iat > 0 && time.Now().Unix() < claims.Iat-int64(clockSkewLeeway.Seconds()) {
        return fmt.Errorf("token not yet valid")
    }
  3. Webhook handler goroutine dùng context.Background() không timeout (channel.go:88-91) — Nếu agent loop hang, goroutine sẽ leak. Nên tạo context với timeout.

  4. buildRequestURL tin tưởng X-Forwarded-* headers (handler.go:92-108) — Nếu không qua reverse proxy, attacker có thể forge để thêm audience hợp lệ. Impact thấp vì project number luôn trong audience list.

  5. UI editHint cho textarea (channel-fields.tsx:62) — Mọi textarea đều hiện credentials hint khi edit. Nếu sau này có textarea không phải secret thì sẽ sai. Hiện tại chỉ có 1 textarea (SA JSON) nên OK.

  6. Thiếu i18n keys cho Google Chat UI fields trong ui/web/src/i18n/locales/.

  7. Thiếu docs hướng dẫn setup: cách tạo SA, enable Chat API, configure webhook URL trong Google Cloud Console.


Overall: PR chất lượng tốt, well-structured. 3 items cần sửa (P0-P2) đều nhỏ và dễ fix.

tungtd1 added 7 commits March 20, 2026 11:37
Prevent memory exhaustion from large response bodies by enforcing
a 1MB limit on API response reads.
Extract IsValidChannelType() as DRY utility in channels package to eliminate
duplicate validation logic across gateway and http channel_instances methods.
Add Iat claim field to verify token has not been issued in the future,
preventing acceptance of tokens not yet valid.
Add google_chat label to channel type display mapping in channels status view.
…ing and RetryDo

- Add format.go: markdownToGoogleChat (Markdown→GChat syntax), chunkByBytes
  (UTF-8-safe splitting at paragraph/line/word/rune boundaries, 3900B limit)
- Fix api.go: wrap HTTP errors with providers.HTTPError for retryable detection,
  add googleChatSendRetryConfig (5 attempts, 1s min delay), wrap SendMessage
  in providers.RetryDo to handle 429/5xx without failing hard
- Fix channel.go: replace byte-unsafe chunking loop (could split mid-UTF-8
  sequence on Vietnamese/emoji text) with chunkByBytes(markdownToGoogleChat(...))
- Add format_test.go: 8 tests covering markdown conversion, ASCII/Vietnamese/
  emoji/single-long-word chunking, paragraph split; all pass with -race
@tuntran
Copy link
Author

tuntran commented Mar 20, 2026

Đã xử lý tất cả feedback từ review

P0–P2 (bắt buộc)

# Issue Fix
P0 io.ReadAll không limit → OOM risk f7c8826 — wrap io.LimitReader(resp.Body, 1<<20)
P1 senderID chứa display name → session mới khi user đổi tên Fixed — extractSenderInfo() dùng user.Name (resource name) làm senderID, displayName truyền riêng qua metadata["sender_name"]
P2 Config comment ghi sai default "open" trong GoogleChatConfig Fixed — comment đã sửa thành "pairing" (default)

Nice-to-have đã làm luôn

# Issue Fix
N1 isValidChannelType duplicate ở 2 packages 8908339 — extract channels.IsValidChannelType() vào internal/channels/channel.go
N2 OIDC thiếu validate iat claim 06a7522 — thêm iat check với clock skew leeway
N6 Thiếu i18n keys cho Google Chat UI fields 1342541 — thêm đủ en/vi/zh translations

Cherry-pick từ PR #196

  • 46e64ef — thay chunking loop cũ (byte-unsafe, có thể cắt giữa UTF-8 sequence) bằng chunkByBytes() với limit 3900B/chunk
  • Thêm markdown → Google Chat format conversion (**bold***bold*, *italic*_italic_, v.v.)
  • SendMessage wrap trong providers.RetryDo (5 attempts, 1s min delay) để xử lý 429/5xx thay vì fail cứng
  • 8 unit tests phủ ASCII/Vietnamese/emoji/single-long-word chunking + race detector clean

Chưa làm (để follow-up PR)

  • N3: Webhook handler context timeout (goroutine leak khi agent loop hang)
  • N4: buildRequestURL trust X-Forwarded-* headers
  • N5: editHint textarea generic issue
  • N7: Docs setup hướng dẫn SA creation, Chat API, webhook config

@tuntran tuntran requested a review from viettranx March 20, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants