337 lines
11 KiB
Go
337 lines
11 KiB
Go
package v1
|
|
|
|
// LINEChannelHandler handles LINE Official Account channel-specific configuration CRUD.
|
|
// Reference: This is a GoChat addition — Chatwoot does not have LINE channel support.
|
|
//
|
|
// LINE Messaging API: https://developers.line.biz/en/docs/messaging-api/
|
|
//
|
|
// gochat maps LINE channel operations to:
|
|
// - POST /api/v1/accounts/:id/channels/line_channel → create LINE inbox
|
|
// - GET /api/v1/accounts/:id/channels/line_channel/:line_id → get LINE channel
|
|
// - PATCH /api/v1/accounts/:id/channels/line_channel/:line_id → update LINE channel
|
|
// - DELETE /api/v1/accounts/:id/channels/line_channel/:line_id → delete LINE channel
|
|
// - GET /api/v1/accounts/:id/channels/line_channel → list LINE channels
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
pkgvalidator "github.com/gochat/gochat/pkg/validator"
|
|
|
|
linechannel "github.com/gochat/gochat/internal/channel/line"
|
|
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// LINEChannelHandler handles LINE Official Account channel management.
|
|
// Follows FacebookChannelHandler pattern: uses top-level ChannelLINEService for CRUD,
|
|
// internal LineProvider for webhook setup via OnCreate.
|
|
type LINEChannelHandler struct {
|
|
lineChannelSvc *service.ChannelLINEService
|
|
lineProvider *linechannel.LineProvider
|
|
inboxSvc *service.InboxService
|
|
lineRepo *repository.ChannelLINERepo
|
|
}
|
|
|
|
// NewLINEChannelHandler creates a new LINE channel handler.
|
|
func NewLINEChannelHandler(
|
|
lineChannelSvc *service.ChannelLINEService,
|
|
lineProvider *linechannel.LineProvider,
|
|
inboxSvc *service.InboxService,
|
|
lineRepo *repository.ChannelLINERepo,
|
|
) *LINEChannelHandler {
|
|
return &LINEChannelHandler{
|
|
lineChannelSvc: lineChannelSvc,
|
|
lineProvider: lineProvider,
|
|
inboxSvc: inboxSvc,
|
|
lineRepo: lineRepo,
|
|
}
|
|
}
|
|
|
|
// === Create ===
|
|
|
|
// CreateLINEChannelRequest is the DTO for creating a LINE channel.
|
|
type CreateLINEChannelRequest struct {
|
|
ChannelID string `json:"channel_id" validate:"required"`
|
|
Name string `json:"name" validate:"required"`
|
|
ChannelAccessToken string `json:"channel_access_token" validate:"required"`
|
|
ChannelSecret string `json:"channel_secret" validate:"required"`
|
|
}
|
|
|
|
// Create adds a new LINE channel and creates the associated inbox.
|
|
// POST /api/v1/accounts/:id/channels/line_channel
|
|
func (h *LINEChannelHandler) Create(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
var req CreateLINEChannelRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
applogger.L().Errorf("Failed to bind LINE channel create request: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
if err := pkgvalidator.ValidateStruct(req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
if err := h.inboxSvc.EnsureCanCreateInbox(c.Request.Context(), accountID); err != nil {
|
|
if renderInboxLimitExceeded(c, err) {
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
|
|
return
|
|
}
|
|
|
|
ch := &channelmodel.ChannelLINE{
|
|
AccountID: accountID,
|
|
ChannelID: req.ChannelID,
|
|
Name: req.Name,
|
|
}
|
|
|
|
if err := h.lineChannelSvc.Create(c.Request.Context(), ch); err != nil {
|
|
applogger.L().Errorf("Failed to create LINE channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create LINE channel"})
|
|
return
|
|
}
|
|
|
|
// Create inbox for the LINE channel
|
|
inboxReq := service.CreateInboxRequest{
|
|
Name: req.Name,
|
|
ChannelType: "line",
|
|
Enabled: true,
|
|
Channel: lineCreateChannelConfig(req),
|
|
}
|
|
inbox, err := h.inboxSvc.Create(c.Request.Context(), accountID, inboxReq)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to create inbox for LINE channel: %v", err)
|
|
if delErr := h.lineChannelSvc.Delete(c.Request.Context(), ch.ID); delErr != nil {
|
|
applogger.L().Warnf("Failed to rollback LINE channel after inbox creation failure: %v", delErr)
|
|
}
|
|
if renderInboxLimitExceeded(c, err) {
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
|
|
return
|
|
}
|
|
|
|
ch.InboxID = inbox.ID
|
|
if err := h.lineChannelSvc.Update(c.Request.Context(), ch); err != nil {
|
|
applogger.L().Errorf("Failed to update LINE channel with inbox_id: %v", err)
|
|
}
|
|
inbox, err = h.inboxSvc.BindChannel(c.Request.Context(), accountID, inbox.ID, ch.ID, inboxReq.Channel)
|
|
if err != nil {
|
|
applogger.L().Warnf("Failed to bind LINE inbox channel config: %v", err)
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeInbox(inbox))
|
|
}
|
|
|
|
// === Get ===
|
|
|
|
// Get retrieves a LINE channel by ID.
|
|
// GET /api/v1/accounts/:id/channels/line_channel/:line_id
|
|
func (h *LINEChannelHandler) Get(c *gin.Context) {
|
|
lineID, err := parseUintParam(c, "line_id")
|
|
if err != nil {
|
|
applogger.L().Errorf("Invalid line_id: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
|
|
return
|
|
}
|
|
|
|
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to get LINE channel: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, h.serializeInboxForLINEChannel(c, ch))
|
|
}
|
|
|
|
// === Update ===
|
|
|
|
// UpdateLINEChannelRequest is the DTO for updating a LINE channel.
|
|
type UpdateLINEChannelRequest struct {
|
|
Name string `json:"name,omitempty"`
|
|
ChannelAccessToken string `json:"channel_access_token,omitempty"`
|
|
ChannelSecret string `json:"channel_secret,omitempty"`
|
|
}
|
|
|
|
// Update modifies an existing LINE channel.
|
|
// PATCH /api/v1/accounts/:id/channels/line_channel/:line_id
|
|
func (h *LINEChannelHandler) Update(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
lineID, err := parseUintParam(c, "line_id")
|
|
if err != nil {
|
|
applogger.L().Errorf("Invalid line_id: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
|
|
return
|
|
}
|
|
|
|
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to get LINE channel: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
|
|
return
|
|
}
|
|
|
|
if ch.AccountID != accountID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "channel does not belong to this account"})
|
|
return
|
|
}
|
|
|
|
var req UpdateLINEChannelRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
applogger.L().Errorf("Failed to bind LINE channel update request: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
if req.Name != "" {
|
|
ch.Name = req.Name
|
|
}
|
|
|
|
if err := h.lineChannelSvc.Update(c.Request.Context(), ch); err != nil {
|
|
applogger.L().Errorf("Failed to update LINE channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update LINE channel"})
|
|
return
|
|
}
|
|
if req.Name != "" {
|
|
if _, inboxErr := h.inboxSvc.Update(c.Request.Context(), accountID, ch.InboxID, service.UpdateInboxRequest{Name: req.Name}); inboxErr != nil {
|
|
applogger.L().Warnf("Failed to update LINE inbox name: %v", inboxErr)
|
|
}
|
|
}
|
|
inbox, bindErr := h.inboxSvc.BindChannel(c.Request.Context(), accountID, ch.InboxID, ch.ID, lineUpdateChannelConfig(req, ch))
|
|
if bindErr != nil {
|
|
applogger.L().Warnf("Failed to update LINE inbox channel config: %v", bindErr)
|
|
c.JSON(http.StatusOK, h.serializeInboxForLINEChannel(c, ch))
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, serializeInbox(inbox))
|
|
}
|
|
|
|
// === Delete ===
|
|
|
|
// Delete removes a LINE channel and its associated inbox.
|
|
// DELETE /api/v1/accounts/:id/channels/line_channel/:line_id
|
|
func (h *LINEChannelHandler) Delete(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
lineID, err := parseUintParam(c, "line_id")
|
|
if err != nil {
|
|
applogger.L().Errorf("Invalid line_id: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
|
|
return
|
|
}
|
|
|
|
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to get LINE channel: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
|
|
return
|
|
}
|
|
|
|
if ch.AccountID != accountID {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "channel does not belong to this account"})
|
|
return
|
|
}
|
|
|
|
// Delete inbox first
|
|
if ch.InboxID > 0 {
|
|
if err := h.inboxSvc.Delete(c.Request.Context(), ch.InboxID); err != nil {
|
|
applogger.L().Errorf("Failed to delete inbox for LINE channel: %v", err)
|
|
}
|
|
}
|
|
|
|
if err := h.lineChannelSvc.Delete(c.Request.Context(), lineID); err != nil {
|
|
applogger.L().Errorf("Failed to delete LINE channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete LINE channel"})
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusOK)
|
|
}
|
|
|
|
// === List ===
|
|
|
|
// List retrieves all LINE channels for an account.
|
|
// GET /api/v1/accounts/:id/channels/line_channel
|
|
func (h *LINEChannelHandler) List(c *gin.Context) {
|
|
accountID := parseAccountIDParam(c)
|
|
if accountID == 0 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
channels, err := h.lineChannelSvc.List(c.Request.Context())
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to list LINE channels: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list LINE channels"})
|
|
return
|
|
}
|
|
|
|
payload := make([]map[string]any, 0, len(channels))
|
|
for i := range channels {
|
|
if channels[i].AccountID == accountID {
|
|
payload = append(payload, h.serializeInboxForLINEChannel(c, &channels[i]))
|
|
}
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"payload": payload})
|
|
}
|
|
|
|
func (h *LINEChannelHandler) serializeInboxForLINEChannel(c *gin.Context, ch *channelmodel.ChannelLINE) map[string]any {
|
|
if ch != nil && ch.InboxID > 0 {
|
|
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
|
|
return serializeInbox(inbox)
|
|
}
|
|
}
|
|
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "channel_id": ch.ChannelID, "name": ch.Name}
|
|
}
|
|
|
|
func lineChannelConfigFromModel(ch *channelmodel.ChannelLINE) map[string]any {
|
|
return map[string]any{
|
|
"line_channel_id": ch.ChannelID,
|
|
"channel_id": ch.ChannelID,
|
|
}
|
|
}
|
|
|
|
func lineCreateChannelConfig(req CreateLINEChannelRequest) map[string]any {
|
|
return map[string]any{
|
|
"line_channel_id": req.ChannelID,
|
|
"line_channel_token": req.ChannelAccessToken,
|
|
"line_channel_secret": req.ChannelSecret,
|
|
"channel_id": req.ChannelID,
|
|
"channel_access_token": req.ChannelAccessToken,
|
|
"channel_secret": req.ChannelSecret,
|
|
}
|
|
}
|
|
|
|
func lineUpdateChannelConfig(req UpdateLINEChannelRequest, ch *channelmodel.ChannelLINE) map[string]any {
|
|
config := lineChannelConfigFromModel(ch)
|
|
if req.ChannelAccessToken != "" {
|
|
config["line_channel_token"] = req.ChannelAccessToken
|
|
config["channel_access_token"] = req.ChannelAccessToken
|
|
}
|
|
if req.ChannelSecret != "" {
|
|
config["line_channel_secret"] = req.ChannelSecret
|
|
config["channel_secret"] = req.ChannelSecret
|
|
}
|
|
return config
|
|
}
|