diff --git a/internal/channels/feishu/bot_policy.go b/internal/channels/feishu/bot_policy.go index a4862195..f91724da 100644 --- a/internal/channels/feishu/bot_policy.go +++ b/internal/channels/feishu/bot_policy.go @@ -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 } } diff --git a/internal/channels/feishu/feishu.go b/internal/channels/feishu/feishu.go index 94c70ac4..294ce98a 100644 --- a/internal/channels/feishu/feishu.go +++ b/internal/channels/feishu/feishu.go @@ -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 } diff --git a/internal/channels/feishu/larkclient_messaging.go b/internal/channels/feishu/larkclient_messaging.go index f828c733..ce568958 100644 --- a/internal/channels/feishu/larkclient_messaging.go +++ b/internal/channels/feishu/larkclient_messaging.go @@ -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 } diff --git a/internal/channels/feishu/larkevents.go b/internal/channels/feishu/larkevents.go index c786d744..ccb3e9d8 100644 --- a/internal/channels/feishu/larkevents.go +++ b/internal/channels/feishu/larkevents.go @@ -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) diff --git a/internal/channels/feishu/larkws.go b/internal/channels/feishu/larkws.go index 02ec451e..603ab603 100644 --- a/internal/channels/feishu/larkws.go +++ b/internal/channels/feishu/larkws.go @@ -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) } diff --git a/internal/channels/feishu/media.go b/internal/channels/feishu/media.go index 1d388ffc..8615a8dc 100644 --- a/internal/channels/feishu/media.go +++ b/internal/channels/feishu/media.go @@ -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) } diff --git a/internal/channels/telegram/reactions.go b/internal/channels/telegram/reactions.go index 4f59b9d6..d6b1e2fa 100644 --- a/internal/channels/telegram/reactions.go +++ b/internal/channels/telegram/reactions.go @@ -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 diff --git a/internal/channels/zalo/personal/channel.go b/internal/channels/zalo/personal/channel.go index 156e5d7d..98d39d9b 100644 --- a/internal/channels/zalo/personal/channel.go +++ b/internal/channels/zalo/personal/channel.go @@ -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 }) diff --git a/internal/channels/zalo/personal/handlers.go b/internal/channels/zalo/personal/handlers.go index c40f768d..910d3f39 100644 --- a/internal/channels/zalo/personal/handlers.go +++ b/internal/channels/zalo/personal/handlers.go @@ -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() diff --git a/internal/channels/zalo/zalo.go b/internal/channels/zalo/zalo.go index 2c5ce04a..88279a4b 100644 --- a/internal/channels/zalo/zalo.go +++ b/internal/channels/zalo/zalo.go @@ -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