438 lines
15 KiB
Go
438 lines
15 KiB
Go
package v1
|
|
|
|
// FacebookChannelHandler handles Facebook channel-specific configuration CRUD and OAuth flow.
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts/channels/facebook_pages_controller.rb
|
|
//
|
|
// Chatwoot's FacebookPagesController provides:
|
|
// - create: authorize FB page → create inbox (via OmniAuth callback or direct page_access_token)
|
|
// - destroy: remove FB page channel + unsubscribe webhook
|
|
// - reauthorize: refresh expired page access tokens
|
|
//
|
|
// gochat maps these to:
|
|
// - POST /api/v1/accounts/:id/channels/facebook_channel → create FB inbox
|
|
// - DELETE /api/v1/accounts/:id/channels/facebook_channel/:fb_id → destroy FB inbox
|
|
// - POST /api/v1/accounts/:id/channels/facebook_channel/reauthorize → refresh token
|
|
// - GET /api/v1/accounts/:id/channels/facebook_channel/authorization → OAuth authorize URL
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
facebookchannel "github.com/gochat/gochat/internal/channel/facebook"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// FacebookChannelHandler handles Facebook Messenger channel management.
|
|
// Follows InstagramChannelHandler pattern: uses top-level ChannelFacebookService for CRUD,
|
|
// internal FacebookProvider for OAuth/authorization.
|
|
type FacebookChannelHandler struct {
|
|
fbChannelSvc *service.ChannelFacebookService
|
|
fbProvider *facebookchannel.FacebookProvider
|
|
inboxSvc *service.InboxService
|
|
fbRepo *repository.ChannelFacebookRepo
|
|
}
|
|
|
|
// NewFacebookChannelHandler creates a new Facebook channel handler.
|
|
func NewFacebookChannelHandler(
|
|
fbChannelSvc *service.ChannelFacebookService,
|
|
fbProvider *facebookchannel.FacebookProvider,
|
|
inboxSvc *service.InboxService,
|
|
fbRepo *repository.ChannelFacebookRepo,
|
|
) *FacebookChannelHandler {
|
|
return &FacebookChannelHandler{
|
|
fbChannelSvc: fbChannelSvc,
|
|
fbProvider: fbProvider,
|
|
inboxSvc: inboxSvc,
|
|
fbRepo: fbRepo,
|
|
}
|
|
}
|
|
|
|
// === OAuth Authorization ===
|
|
|
|
// FacebookAuthorizationRequest is the DTO for initiating FB OAuth flow.
|
|
type FacebookAuthorizationRequest struct {
|
|
RedirectURL string `json:"redirect_url" validate:"required,url"`
|
|
}
|
|
|
|
// Authorization generates a Facebook OAuth authorize URL.
|
|
// GET /api/v1/accounts/:id/channels/facebook_channel/authorization
|
|
func (h *FacebookChannelHandler) Authorization(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
applogger.L().Errorf("Invalid account_id: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
var req FacebookAuthorizationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
redirectURL := c.Query("redirect_url")
|
|
if redirectURL == "" {
|
|
applogger.L().Errorf("Failed to bind authorization request: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "redirect_url is required"})
|
|
return
|
|
}
|
|
req.RedirectURL = redirectURL
|
|
}
|
|
|
|
authURL, err := h.fbProvider.BuildAuthURL(c.Request.Context(), uint(accountID), req.RedirectURL)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to build Facebook auth URL: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to generate authorization URL"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"authorization_url": authURL,
|
|
"account_id": accountID,
|
|
})
|
|
}
|
|
|
|
// FacebookOAuthCallbackRequest is the DTO for the OAuth callback after FB redirects back.
|
|
type FacebookOAuthCallbackRequest struct {
|
|
Code string `json:"code" validate:"required"`
|
|
RedirectURL string `json:"redirect_url" validate:"required,url"`
|
|
}
|
|
|
|
// OAuthCallback exchanges the Facebook OAuth code for a long-lived access token.
|
|
// POST /api/v1/accounts/:id/channels/facebook_channel/oauth_callback
|
|
func (h *FacebookChannelHandler) OAuthCallback(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
applogger.L().Errorf("Invalid account_id: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
var req FacebookOAuthCallbackRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
applogger.L().Errorf("Failed to bind OAuth callback request: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "code and redirect_url are required"})
|
|
return
|
|
}
|
|
|
|
tokenResult, err := h.fbProvider.ExchangeToken(c.Request.Context(), req.Code, req.RedirectURL)
|
|
if err != nil {
|
|
applogger.L().Errorf("Facebook token exchange failed: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to exchange token"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"access_token": tokenResult.AccessToken,
|
|
"expires_at": tokenResult.ExpiresAt,
|
|
"account_id": accountID,
|
|
"message": "token obtained; use CreateFacebookPage endpoint to create inbox with page_access_token",
|
|
})
|
|
}
|
|
|
|
// === Facebook Channel CRUD ===
|
|
|
|
// CreateFacebookPageRequest is the DTO for creating a Facebook Messenger inbox.
|
|
type CreateFacebookPageRequest struct {
|
|
Name string `json:"name" validate:"required,min=2"`
|
|
PageID string `json:"page_id" validate:"required"`
|
|
PageAccessToken string `json:"page_access_token" validate:"required"`
|
|
WebhookVerifyToken string `json:"webhook_verify_token,omitempty"` // auto-generated if empty
|
|
EnableAutoAssignment bool `json:"enable_auto_assignment,omitempty"`
|
|
}
|
|
|
|
// CreateFacebookPage creates a new Facebook Messenger inbox for a Facebook Page.
|
|
// POST /api/v1/accounts/:id/channels/facebook_channel
|
|
func (h *FacebookChannelHandler) CreateFacebookPage(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
applogger.L().Errorf("Invalid account_id: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
var req CreateFacebookPageRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
applogger.L().Errorf("Failed to bind create facebook page request: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Delegate to InboxService.CreateFacebookInbox (matches Instagram pattern)
|
|
inboxReq := service.CreateFacebookInboxRequest{
|
|
Name: req.Name,
|
|
PageID: req.PageID,
|
|
PageAccessToken: req.PageAccessToken,
|
|
PageName: "", // populated after Graph API call or update
|
|
WebhookVerifyToken: req.WebhookVerifyToken,
|
|
EnableAutoAssignment: req.EnableAutoAssignment,
|
|
}
|
|
|
|
inbox, err := h.inboxSvc.CreateFacebookInbox(ctx, uint(accountID), inboxReq, h.fbRepo)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to create Facebook inbox: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create Facebook inbox", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"inbox": gin.H{
|
|
"id": inbox.ID,
|
|
"name": inbox.Name,
|
|
"channel_type": inbox.ChannelType,
|
|
"channel_id": inbox.ChannelID,
|
|
"enabled": inbox.Enabled,
|
|
"enable_auto_assignment": inbox.EnableAutoAssignment,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetFacebookChannel retrieves a Facebook channel by ID.
|
|
// GET /api/v1/accounts/:id/channels/facebook_channel/:fb_id
|
|
func (h *FacebookChannelHandler) GetFacebookChannel(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
fbIDStr := c.Param("fb_id")
|
|
fbID, err := strconv.ParseUint(fbIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid fb_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
ch, err := h.fbChannelSvc.GetByID(ctx, uint(fbID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to get Facebook channel: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Facebook channel not found"})
|
|
return
|
|
}
|
|
|
|
// Verify account ownership
|
|
if ch.AccountID != uint(accountID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Facebook channel does not belong to this account"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"id": ch.ID,
|
|
"account_id": ch.AccountID,
|
|
"inbox_id": ch.InboxID,
|
|
"page_id": ch.PageID,
|
|
"page_name": ch.PageName,
|
|
"app_id": ch.AppID,
|
|
"reauthorization_required": ch.ReauthorizationRequired,
|
|
})
|
|
}
|
|
|
|
// ListFacebookChannels retrieves all Facebook channels for an account.
|
|
// GET /api/v1/accounts/:id/channels/facebook_channel
|
|
func (h *FacebookChannelHandler) ListFacebookChannels(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
channels, err := h.fbChannelSvc.ListByAccount(ctx, uint(accountID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to list Facebook channels: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Facebook channels"})
|
|
return
|
|
}
|
|
|
|
result := make([]gin.H, 0, len(channels))
|
|
for _, ch := range channels {
|
|
result = append(result, gin.H{
|
|
"id": ch.ID,
|
|
"account_id": ch.AccountID,
|
|
"inbox_id": ch.InboxID,
|
|
"page_id": ch.PageID,
|
|
"page_name": ch.PageName,
|
|
"app_id": ch.AppID,
|
|
"reauthorization_required": ch.ReauthorizationRequired,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"channels": result,
|
|
"count": len(result),
|
|
})
|
|
}
|
|
|
|
// DeleteFacebookPage removes a Facebook Page channel and its inbox.
|
|
// DELETE /api/v1/accounts/:id/channels/facebook_channel/:fb_id
|
|
func (h *FacebookChannelHandler) DeleteFacebookPage(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
fbIDStr := c.Param("fb_id")
|
|
fbID, err := strconv.ParseUint(fbIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid fb_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
ch, err := h.fbChannelSvc.GetByID(ctx, uint(fbID))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Facebook channel not found"})
|
|
return
|
|
}
|
|
|
|
// Verify account ownership
|
|
if ch.AccountID != uint(accountID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Facebook channel does not belong to this account"})
|
|
return
|
|
}
|
|
|
|
// Delete the channel record
|
|
if err := h.fbChannelSvc.Delete(ctx, uint(fbID)); err != nil {
|
|
applogger.L().Errorf("Failed to delete Facebook channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete Facebook channel"})
|
|
return
|
|
}
|
|
|
|
// Delete the associated inbox (if exists)
|
|
if ch.InboxID > 0 {
|
|
if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil {
|
|
applogger.L().Warnf("Failed to delete inbox for Facebook channel: %v", delErr)
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Facebook page channel deleted successfully"})
|
|
}
|
|
|
|
// === Reauthorization ===
|
|
|
|
// ReauthorizeFacebookPageRequest is the DTO for refreshing a FB page access token.
|
|
type ReauthorizeFacebookPageRequest struct {
|
|
PageAccessToken string `json:"page_access_token" validate:"required"`
|
|
}
|
|
|
|
// ReauthorizeFacebookPage refreshes an expired Facebook Page access token.
|
|
// POST /api/v1/accounts/:id/channels/facebook_channel/reauthorize
|
|
func (h *FacebookChannelHandler) ReauthorizeFacebookPage(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
var req ReauthorizeFacebookPageRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
applogger.L().Errorf("Failed to bind reauthorize request: %v", err)
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Find all Facebook channels for this account and update their tokens
|
|
channels, listErr := h.fbChannelSvc.ListByAccount(ctx, uint(accountID))
|
|
if listErr != nil {
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Facebook channels"})
|
|
return
|
|
}
|
|
|
|
for _, ch := range channels {
|
|
ch.PageAccessToken = req.PageAccessToken
|
|
ch.ReauthorizationRequired = false
|
|
if updateErr := h.fbRepo.Update(ctx, &ch); updateErr != nil {
|
|
applogger.L().Warnf("Failed to update token for channel %d: %v", ch.ID, updateErr)
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Facebook page access token refreshed successfully"})
|
|
}
|
|
|
|
// GetFacebookPage retrieves a Facebook channel page by ID.
|
|
// GET /api/v1/accounts/:id/channels/facebook_channel/:fb_id
|
|
// Alias for GetFacebookChannel — router registers both names.
|
|
func (h *FacebookChannelHandler) GetFacebookPage(c *gin.Context) {
|
|
h.GetFacebookChannel(c)
|
|
}
|
|
|
|
// UpdateFacebookPage updates a Facebook channel page configuration.
|
|
// PATCH /api/v1/accounts/:id/channels/facebook_channel/:fb_id
|
|
func (h *FacebookChannelHandler) UpdateFacebookPage(c *gin.Context) {
|
|
accountIDStr := c.Param("id")
|
|
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
|
|
return
|
|
}
|
|
|
|
fbIDStr := c.Param("fb_id")
|
|
fbID, err := strconv.ParseUint(fbIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid fb_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
ch, err := h.fbChannelSvc.GetByID(ctx, uint(fbID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to get Facebook channel for update: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Facebook channel not found"})
|
|
return
|
|
}
|
|
|
|
// Verify account ownership
|
|
if ch.AccountID != uint(accountID) {
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Facebook channel does not belong to this account"})
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
PageName *string `json:"page_name"`
|
|
ReauthorizationRequired *bool `json:"reauthorization_required"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
|
return
|
|
}
|
|
|
|
updateReq := service.UpdateFacebookChannelRequest{
|
|
PageName: req.PageName,
|
|
ReauthorizationRequired: req.ReauthorizationRequired,
|
|
}
|
|
|
|
updated, err := h.fbChannelSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to update Facebook channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update Facebook channel"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"id": updated.ID,
|
|
"account_id": updated.AccountID,
|
|
"inbox_id": updated.InboxID,
|
|
"page_id": updated.PageID,
|
|
"page_name": updated.PageName,
|
|
"reauthorization_required": updated.ReauthorizationRequired,
|
|
})
|
|
}
|
|
|