mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 12:14:24 +00:00
fix(channels): harden error handling and type safety across Telegram, Zalo, Feishu (#247)
Telegram: - Fix context cancellation in reaction timer callbacks — use context.Background() so emoji reactions still work after request context is cancelled - Add comma-ok safety on sync.Map type assertions in OnReactionEvent/ClearReaction Zalo: - Add comma-ok safety on sync.Map type assertions for typing controllers - Validate non-empty senderID before processing text/image messages Feishu/Lark: - Handle json.Unmarshal errors in all 7 LarkClient messaging methods instead of silently returning empty values - Add 30s HTTP timeout for WebSocket endpoint request (was using DefaultClient) - Eliminate double decryption in webhook handler — cache first result - Replace goroutine+sleep with time.AfterFunc for dedup cleanup - Add comma-ok safety on pairing debounce type assertion - Use json.Marshal instead of fmt.Sprintf for image/file key JSON construction Co-authored-by: Luvu182 <208665161+Luvu182@users.noreply.github.com>
This commit is contained in:
1 parent
06e995ce03
commit
8cbd30f1ab
10 files changed
+64
-33
No files matched your search
@@ -151,7 +151,7 @@ func (c *Channel) sendPairingReply(senderID, chatID string) {
|
||||
|
||||
// Debounce
|
||||
if lastSent, ok := c.pairingDebounce.Load(senderID); ok {
|
||||
if time.Since(lastSent.(time.Time)) < pairingDebounceTime {
|
||||
if t, ok := lastSent.(time.Time); ok && time.Since(t) < pairingDebounceTime {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,10 +440,9 @@ func shouldUseCard(text string) bool {
|
||||
func (c *Channel) isDuplicate(messageID string) bool {
|
||||
_, loaded := c.dedup.LoadOrStore(messageID, struct{}{})
|
||||
if !loaded {
|
||||
go func() {
|
||||
time.Sleep(5 * time.Minute)
|
||||
time.AfterFunc(5*time.Minute, func() {
|
||||
c.dedup.Delete(messageID)
|
||||
}()
|
||||
})
|
||||
}
|
||||
return loaded
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ func (c *LarkClient) SendMessage(ctx context.Context, receiveIDType, receiveID,
|
||||
return nil, fmt.Errorf("send message: code=%d msg=%s", resp.Code, resp.Msg)
|
||||
}
|
||||
var data SendMessageResp
|
||||
json.Unmarshal(resp.Data, &data)
|
||||
if err := json.Unmarshal(resp.Data, &data); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
@@ -55,7 +57,9 @@ func (c *LarkClient) UploadImage(ctx context.Context, data io.Reader) (string, e
|
||||
var result struct {
|
||||
ImageKey string `json:"image_key"`
|
||||
}
|
||||
json.Unmarshal(resp.Data, &result)
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return "", fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
return result.ImageKey, nil
|
||||
}
|
||||
|
||||
@@ -79,7 +83,9 @@ func (c *LarkClient) UploadFile(ctx context.Context, data io.Reader, fileName, f
|
||||
var result struct {
|
||||
FileKey string `json:"file_key"`
|
||||
}
|
||||
json.Unmarshal(resp.Data, &result)
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return "", fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
return result.FileKey, nil
|
||||
}
|
||||
|
||||
@@ -106,7 +112,9 @@ func (c *LarkClient) CreateCard(ctx context.Context, cardType, data string) (str
|
||||
var result struct {
|
||||
CardID string `json:"card_id"`
|
||||
}
|
||||
json.Unmarshal(resp.Data, &result)
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return "", fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
return result.CardID, nil
|
||||
}
|
||||
|
||||
@@ -165,7 +173,9 @@ func (c *LarkClient) AddMessageReaction(ctx context.Context, messageID, emojiTyp
|
||||
var result struct {
|
||||
ReactionID string `json:"reaction_id"`
|
||||
}
|
||||
json.Unmarshal(resp.Data, &result)
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return "", fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
return result.ReactionID, nil
|
||||
}
|
||||
|
||||
@@ -200,7 +210,9 @@ func (c *LarkClient) GetBotInfo(ctx context.Context) (string, error) {
|
||||
OpenID string `json:"open_id"`
|
||||
} `json:"bot"`
|
||||
}
|
||||
json.Unmarshal(resp.Data, &result)
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return "", fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
return result.Bot.OpenID, nil
|
||||
}
|
||||
|
||||
@@ -220,6 +232,8 @@ func (c *LarkClient) GetUser(ctx context.Context, userID, userIDType string) (st
|
||||
Name string `json:"name"`
|
||||
} `json:"user"`
|
||||
}
|
||||
json.Unmarshal(resp.Data, &result)
|
||||
if err := json.Unmarshal(resp.Data, &result); err != nil {
|
||||
return "", fmt.Errorf("unmarshal response: %w", err)
|
||||
}
|
||||
return result.User.Name, nil
|
||||
}
|
||||
@@ -105,6 +105,7 @@ func NewWebhookHandler(verificationToken, encryptKey string, onMessage func(even
|
||||
}
|
||||
|
||||
// Handle encrypted events
|
||||
eventBody := body
|
||||
if envelope.Encrypt != "" && encryptKey != "" {
|
||||
decrypted, err := decryptEvent(envelope.Encrypt, encryptKey)
|
||||
if err != nil {
|
||||
@@ -112,6 +113,7 @@ func NewWebhookHandler(verificationToken, encryptKey string, onMessage func(even
|
||||
http.Error(w, "decrypt failed", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
eventBody = decrypted
|
||||
// Re-parse decrypted content
|
||||
if err := json.Unmarshal(decrypted, &envelope); err != nil {
|
||||
http.Error(w, "invalid decrypted json", http.StatusBadRequest)
|
||||
@@ -128,14 +130,6 @@ func NewWebhookHandler(verificationToken, encryptKey string, onMessage func(even
|
||||
|
||||
// Parse as message event
|
||||
var event MessageEvent
|
||||
// Re-unmarshal the original (or decrypted) body as a full event
|
||||
eventBody := body
|
||||
if envelope.Encrypt != "" && encryptKey != "" {
|
||||
decrypted, _ := decryptEvent(envelope.Encrypt, encryptKey)
|
||||
if decrypted != nil {
|
||||
eventBody = decrypted
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(eventBody, &event); err != nil {
|
||||
slog.Debug("feishu webhook parse event failed", "error", err)
|
||||
|
||||
@@ -189,7 +189,8 @@ func (c *WSClient) getWSEndpoint(ctx context.Context) (string, error) {
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ws endpoint request: %w", err)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package feishu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -35,8 +36,11 @@ func (c *Channel) uploadFile(ctx context.Context, data io.Reader, fileName, file
|
||||
|
||||
// sendImage sends an image message using an image_key.
|
||||
func (c *Channel) sendImage(ctx context.Context, chatID, receiveIDType, imageKey string) error {
|
||||
content := fmt.Sprintf(`{"image_key":"%s"}`, imageKey)
|
||||
_, err := c.client.SendMessage(ctx, receiveIDType, chatID, "image", content)
|
||||
contentBytes, err := json.Marshal(map[string]string{"image_key": imageKey})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal image content: %w", err)
|
||||
}
|
||||
_, err = c.client.SendMessage(ctx, receiveIDType, chatID, "image", string(contentBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("feishu send image: %w", err)
|
||||
}
|
||||
@@ -49,8 +53,11 @@ func (c *Channel) sendFile(ctx context.Context, chatID, receiveIDType, fileKey,
|
||||
if msgType == "" {
|
||||
msgType = "file"
|
||||
}
|
||||
content := fmt.Sprintf(`{"file_key":"%s"}`, fileKey)
|
||||
_, err := c.client.SendMessage(ctx, receiveIDType, chatID, msgType, content)
|
||||
contentBytes, err := json.Marshal(map[string]string{"file_key": fileKey})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal file content: %w", err)
|
||||
}
|
||||
_, err = c.client.SendMessage(ctx, receiveIDType, chatID, msgType, string(contentBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("feishu send file: %w", err)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func (rc *StatusReactionController) SetStatus(ctx context.Context, status string
|
||||
|
||||
emoji := resolveReactionEmoji(rc.lastStatus, nil)
|
||||
if emoji != "" {
|
||||
rc.applyReaction(ctx, emoji)
|
||||
rc.applyReaction(context.Background(), emoji)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (rc *StatusReactionController) applyReaction(ctx context.Context, emoji str
|
||||
}
|
||||
|
||||
// resetStallTimer resets the stall detection timer (must hold mu lock).
|
||||
func (rc *StatusReactionController) resetStallTimer(ctx context.Context) {
|
||||
func (rc *StatusReactionController) resetStallTimer(_ context.Context) {
|
||||
rc.cancelStall()
|
||||
|
||||
rc.stallTimer = time.AfterFunc(stallSoftMs, func() {
|
||||
@@ -193,7 +193,7 @@ func (rc *StatusReactionController) resetStallTimer(ctx context.Context) {
|
||||
|
||||
emoji := resolveReactionEmoji("stallSoft", nil)
|
||||
if emoji != "" {
|
||||
rc.applyReaction(ctx, emoji)
|
||||
rc.applyReaction(context.Background(), emoji)
|
||||
}
|
||||
|
||||
// Schedule hard stall
|
||||
@@ -207,7 +207,7 @@ func (rc *StatusReactionController) resetStallTimer(ctx context.Context) {
|
||||
|
||||
emoji := resolveReactionEmoji("stallHard", nil)
|
||||
if emoji != "" {
|
||||
rc.applyReaction(ctx, emoji)
|
||||
rc.applyReaction(context.Background(), emoji)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -258,7 +258,10 @@ func (c *Channel) OnReactionEvent(ctx context.Context, chatID string, messageID
|
||||
// Key by messageID so concurrent runs in the same chat don't clash.
|
||||
key := fmt.Sprintf("%s:%s", chatID, messageID)
|
||||
val, _ := c.reactions.LoadOrStore(key, newStatusReactionController(c.bot, id, msgID))
|
||||
rc := val.(*StatusReactionController)
|
||||
rc, ok := val.(*StatusReactionController)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
rc.SetStatus(ctx, status)
|
||||
|
||||
@@ -285,8 +288,9 @@ func (c *Channel) ClearReaction(ctx context.Context, chatID string, messageID st
|
||||
// Stop and remove controller if exists
|
||||
key := fmt.Sprintf("%s:%s", chatID, messageID)
|
||||
if val, ok := c.reactions.LoadAndDelete(key); ok {
|
||||
rc := val.(*StatusReactionController)
|
||||
rc.Stop()
|
||||
if rc, ok := val.(*StatusReactionController); ok {
|
||||
rc.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Clear reaction on the message
|
||||
|
||||
@@ -134,7 +134,9 @@ func (c *Channel) Stop(_ context.Context) error {
|
||||
slog.Info("stopping zalo_personal channel")
|
||||
c.stopOnce.Do(func() { close(c.stopCh) })
|
||||
c.typingCtrls.Range(func(key, val any) bool {
|
||||
val.(*typing.Controller).Stop()
|
||||
if ctrl, ok := val.(*typing.Controller); ok {
|
||||
ctrl.Stop()
|
||||
}
|
||||
c.typingCtrls.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -147,7 +147,9 @@ func (c *Channel) startTyping(threadID string, threadType protocol.ThreadType) {
|
||||
},
|
||||
})
|
||||
if prev, ok := c.typingCtrls.Load(threadID); ok {
|
||||
prev.(*typing.Controller).Stop()
|
||||
if ctrl, ok := prev.(*typing.Controller); ok {
|
||||
ctrl.Stop()
|
||||
}
|
||||
}
|
||||
c.typingCtrls.Store(threadID, ctrl)
|
||||
ctrl.Start()
|
||||
|
||||
@@ -187,6 +187,10 @@ func (c *Channel) processUpdate(update zaloUpdate) {
|
||||
|
||||
func (c *Channel) handleTextMessage(msg *zaloMessage) {
|
||||
senderID := msg.From.ID
|
||||
if senderID == "" {
|
||||
slog.Warn("zalo: dropping text message with empty sender ID", "message_id", msg.MessageID)
|
||||
return
|
||||
}
|
||||
chatID := msg.Chat.ID
|
||||
if chatID == "" {
|
||||
chatID = senderID
|
||||
@@ -218,6 +222,10 @@ func (c *Channel) handleTextMessage(msg *zaloMessage) {
|
||||
|
||||
func (c *Channel) handleImageMessage(msg *zaloMessage) {
|
||||
senderID := msg.From.ID
|
||||
if senderID == "" {
|
||||
slog.Warn("zalo: dropping image message with empty sender ID", "message_id", msg.MessageID)
|
||||
return
|
||||
}
|
||||
chatID := msg.Chat.ID
|
||||
if chatID == "" {
|
||||
chatID = senderID
|
||||
|
||||
Reference in new issue
Block a user