133 lines
4.2 KiB
Go
133 lines
4.2 KiB
Go
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
|
|
// Method: POST (LINE Messaging API sends events as JSON)
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
linechannel "github.com/gochat/gochat/internal/channel/line"
|
|
"github.com/gochat/gochat/internal/model"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// LineWebhookHandler processes LINE webhook requests via Gin.
|
|
type LineWebhookHandler struct {
|
|
lineWebhook *linechannel.WebhookHandler
|
|
pipeline *linechannel.IncomingProcessor
|
|
service *linechannel.LineService
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewLineWebhookHandler creates a LINE webhook handler for Gin integration.
|
|
func NewLineWebhookHandler(lineWebhook *linechannel.WebhookHandler, pipeline *linechannel.IncomingProcessor, service *linechannel.LineService, db *gorm.DB) *LineWebhookHandler {
|
|
return &LineWebhookHandler{
|
|
lineWebhook: lineWebhook,
|
|
pipeline: pipeline,
|
|
service: service,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
|
|
// Lookup inbox from database
|
|
inbox, err := h.lookupInbox(uint(inboxID))
|
|
if err != nil {
|
|
applogger.L().Warnf("LINE webhook: inbox lookup failed for id %d: %v", inboxID, err)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
|
|
// 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)
|
|
c.JSON(http.StatusOK, gin.H{"status": "ignored"})
|
|
return
|
|
}
|
|
defer c.Request.Body.Close()
|
|
|
|
// Verify webhook signature
|
|
config := h.parseChannelConfig(inbox)
|
|
channelSecret := ""
|
|
if v, ok := config["channel_secret"].(string); ok {
|
|
channelSecret = v
|
|
}
|
|
signature := c.GetHeader("X-Line-Signature")
|
|
|
|
if channelSecret != "" && signature != "" {
|
|
if !h.service.VerifySignature(channelSecret, string(body), signature) {
|
|
applogger.L().Warnf("LINE webhook: invalid signature for inbox=%d", inboxID)
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid signature"})
|
|
return
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"})
|
|
return
|
|
}
|
|
|
|
// Process each event via the pipeline
|
|
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)
|
|
continue
|
|
}
|
|
if incomingMsg != nil {
|
|
applogger.L().Debugf("LINE webhook: processed event type=%s sender=%s",
|
|
event.Type, incomingMsg.SenderID)
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "processed"})
|
|
}
|
|
|
|
// HandleLineVerification responds to LINE webhook URL verification.
|
|
func (h *LineWebhookHandler) HandleLineVerification(c *gin.Context) {
|
|
c.JSON(http.StatusOK, gin.H{"status": "verified"})
|
|
}
|
|
|
|
// lookupInbox fetches an Inbox record from the database.
|
|
func (h *LineWebhookHandler) lookupInbox(inboxID uint) (*model.Inbox, error) {
|
|
var inbox model.Inbox
|
|
if err := h.db.Where("id = ?", inboxID).First(&inbox).Error; err != nil {
|
|
return nil, 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 == "" {
|
|
return map[string]interface{}{}
|
|
}
|
|
var config map[string]interface{}
|
|
if err := json.Unmarshal([]byte(inbox.ChannelConfig), &config); err != nil {
|
|
applogger.L().Warnf("LINE: Failed to parse ChannelConfig for inbox %d: %v", inbox.ID, err)
|
|
return map[string]interface{}{}
|
|
}
|
|
return config
|
|
} |