feat(googlechat): Google Chat channel integration#148
feat(googlechat): Google Chat channel integration#148tuntran wants to merge 13 commits intonextlevelbuilder:mainfrom
Conversation
viettranx
left a comment
There was a problem hiding this comment.
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.go — os.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.go — val.(*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. 👍
- 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
|
All review feedback addressed in c29b075: Critical
Important
Suggestions
Ready for re-review. |
…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
019d018 to
6c3cec0
Compare
viettranx
left a comment
There was a problem hiding this comment.
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
Managerhỗ 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 trongconfig_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)) // 1MBP1 — senderID chứa display name (handler.go:128-141)
senderID = user.Name + "|" + displayNameNế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-47 và factory.go:67-69 set default "pairing". Sửa comment cho khớp.
Nice-to-have (follow-up PR OK)
-
isValidChannelTypebị duplicate — Cùng hàm giống hệt ởinternal/gateway/methods/channel_instances.govàinternal/http/channel_instances.go. Nên refactor thành 1 hàmIsValidChannelType()tronginternal/channels/channel.godùng constants đã có. -
OIDC thiếu validate
iatclaim — Chỉ checkexp, không checkiat. Token vớiiattrong 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") }
-
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. -
buildRequestURLtin tưởngX-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. -
UI
editHintcho 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. -
Thiếu i18n keys cho Google Chat UI fields trong
ui/web/src/i18n/locales/. -
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.
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
Đã xử lý tất cả feedback từ reviewP0–P2 (bắt buộc)
Nice-to-have đã làm luôn
Cherry-pick từ PR #196
Chưa làm (để follow-up PR)
|
Summary
google_chatto valid channel types allowlistConvention Fixes (vs PR #135)
"pairing"for DB instances (security parity with Discord/Feishu/Slack)channels.TypeGoogleChatconstant ("google_chat")project_number)resolveServiceAccountJSONtocredentials.go(channel.go under 200 lines)Changes
internal/channels/googlechat/— full channel implementation (webhook handler, bot API, factory, types, credentials)internal/config/config_channels.go— addGoogleChatConfigstruct withservice_account_jsonfieldinternal/channels/channel.go— addTypeGoogleChatconstantcmd/gateway.go— register GoogleChat factory in instance loadercmd/gateway_channels_setup.go— add GoogleChat to config-based channel registrationui/web/src/pages/channels/— UI fields for GoogleChat channel setupinternal/config/config_secrets.go— mark SA JSON as sensitive