feat(webhook): align chatwoot ingress routes

This commit is contained in:
2026-06-04 23:20:58 +08:00
parent 6aa62c6d7b
commit 9e3f561bed
12 changed files with 467 additions and 120 deletions
+35 -13
View File
@@ -3,17 +3,18 @@ package webhook
// LineWebhookHandler processes incoming LINE webhook HTTP requests via Gin.
// Reference: Facebook webhook adapter pattern (facebook_webhook.go)
//
// URL pattern: /webhooks/line/:inbox_id
// URL pattern: /webhooks/line/:line_channel_id
// Method: POST (LINE Messaging API sends events as JSON)
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
linechannel "github.com/gochat/gochat/internal/channel/line"
"github.com/gochat/gochat/internal/model"
channelmodel "github.com/gochat/gochat/internal/model/channel"
applogger "github.com/gochat/gochat/pkg/logger"
"github.com/gin-gonic/gin"
@@ -40,18 +41,20 @@ func NewLineWebhookHandler(lineWebhook *linechannel.WebhookHandler, pipeline *li
// HandleLineWebhook processes an incoming LINE webhook Gin request.
func (h *LineWebhookHandler) HandleLineWebhook(c *gin.Context) {
inboxIDStr := c.Param("inbox_id")
inboxID, err := strconv.ParseUint(inboxIDStr, 10, 32)
if err != nil {
applogger.L().Warnf("LINE webhook: invalid inbox_id %s", inboxIDStr)
lineChannelID := c.Param("line_channel_id")
if lineChannelID == "" {
lineChannelID = c.Param("channel_id")
}
if lineChannelID == "" {
applogger.L().Warn("LINE webhook: missing line_channel_id in path")
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
// Lookup inbox from database
inbox, err := h.lookupInbox(uint(inboxID))
inbox, err := h.lookupInboxByLineChannelID(lineChannelID)
if err != nil {
applogger.L().Warnf("LINE webhook: inbox lookup failed for id %d: %v", inboxID, err)
applogger.L().Warnf("LINE webhook: inbox lookup failed for channel_id %s: %v", lineChannelID, err)
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
@@ -59,7 +62,7 @@ func (h *LineWebhookHandler) HandleLineWebhook(c *gin.Context) {
// Read request body
body, err := io.ReadAll(c.Request.Body)
if err != nil {
applogger.L().Errorf("LINE webhook: failed to read body for inbox %d: %v", inboxID, err)
applogger.L().Errorf("LINE webhook: failed to read body for inbox %d: %v", inbox.ID, err)
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
return
}
@@ -75,7 +78,7 @@ func (h *LineWebhookHandler) HandleLineWebhook(c *gin.Context) {
if channelSecret != "" && signature != "" {
if !h.service.VerifySignature(channelSecret, string(body), signature) {
applogger.L().Warnf("LINE webhook: invalid signature for inbox=%d", inboxID)
applogger.L().Warnf("LINE webhook: invalid signature for inbox=%d", inbox.ID)
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
return
}
@@ -84,7 +87,7 @@ func (h *LineWebhookHandler) HandleLineWebhook(c *gin.Context) {
// Parse webhook event
var webhookEvent linechannel.WebhookEvent
if err := json.Unmarshal(body, &webhookEvent); err != nil {
applogger.L().Errorf("LINE webhook: failed to parse JSON for inbox %d: %v", inboxID, err)
applogger.L().Errorf("LINE webhook: failed to parse JSON for inbox %d: %v", inbox.ID, err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
return
}
@@ -93,7 +96,7 @@ func (h *LineWebhookHandler) HandleLineWebhook(c *gin.Context) {
for _, event := range webhookEvent.Events {
incomingMsg, err := h.pipeline.ProcessEvent(c.Request.Context(), inbox, event)
if err != nil {
applogger.L().Errorf("LINE webhook: process event failed for inbox %d: %v", inboxID, err)
applogger.L().Errorf("LINE webhook: process event failed for inbox %d: %v", inbox.ID, err)
continue
}
if incomingMsg != nil {
@@ -119,6 +122,25 @@ func (h *LineWebhookHandler) lookupInbox(inboxID uint) (*model.Inbox, error) {
return &inbox, nil
}
// lookupInboxByLineChannelID fetches an Inbox through the LINE channel record.
// Chatwoot exposes /webhooks/line/:line_channel_id and resolves the channel from that URL segment.
func (h *LineWebhookHandler) lookupInboxByLineChannelID(lineChannelID string) (*model.Inbox, error) {
if h.db == nil {
return nil, fmt.Errorf("line webhook database is not configured")
}
var channel channelmodel.ChannelLINE
if err := h.db.Where("channel_id = ?", lineChannelID).First(&channel).Error; err != nil {
return nil, fmt.Errorf("line channel not found for channel_id=%s: %w", lineChannelID, err)
}
var inbox model.Inbox
if err := h.db.Where("id = ? AND channel_type = ?", channel.InboxID, "line").First(&inbox).Error; err != nil {
return nil, fmt.Errorf("line inbox not found for channel inbox_id=%d: %w", channel.InboxID, err)
}
return &inbox, nil
}
// parseChannelConfig parses the JSON-encoded ChannelConfig string into a map.
func (h *LineWebhookHandler) parseChannelConfig(inbox *model.Inbox) map[string]interface{} {
if inbox.ChannelConfig == "" {
@@ -130,4 +152,4 @@ func (h *LineWebhookHandler) parseChannelConfig(inbox *model.Inbox) map[string]i
return map[string]interface{}{}
}
return config
}
}