feat(channels): align facebook callbacks

This commit is contained in:
2026-06-06 22:18:33 +08:00
parent e730b28bbd
commit 6e66af78c7
12 changed files with 599 additions and 63 deletions
@@ -0,0 +1,164 @@
package v1
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
facebookchannel "github.com/gochat/gochat/internal/channel/facebook"
"github.com/gochat/gochat/internal/model"
channelmodel "github.com/gochat/gochat/internal/model/channel"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
)
type fakeFacebookCallbackProvider struct {
longToken string
pages []facebookchannel.FBPageInfo
instagramID string
}
func (f *fakeFacebookCallbackProvider) ExchangeLongLivedUserToken(context.Context, string) (string, error) {
return f.longToken, nil
}
func (f *fakeFacebookCallbackProvider) ListFacebookPages(context.Context, string) ([]facebookchannel.FBPageInfo, error) {
return f.pages, nil
}
func (f *fakeFacebookCallbackProvider) FetchInstagramBusinessAccountID(context.Context, string) (string, error) {
return f.instagramID, nil
}
func setupFacebookCallbackTest(t *testing.T, provider *fakeFacebookCallbackProvider) (*gin.Engine, *gorm.DB) {
t.Helper()
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}, &channelmodel.ChannelFacebook{}))
inboxRepo := repository.NewInboxRepo(db)
fbRepo := repository.NewChannelFacebookRepo(db)
inboxSvc := service.NewInboxService(inboxRepo, nil, nil, nil, nil, nil, nil)
fbSvc := service.NewChannelFacebookService(fbRepo)
handler := NewFacebookChannelHandler(fbSvc, nil, inboxSvc, fbRepo, provider)
r := gin.New()
r.POST("/api/v1/accounts/:account_id/callbacks/register_facebook_page", handler.RegisterFacebookPage)
r.GET("/api/v1/accounts/:account_id/callbacks/register_facebook_page", handler.RegisterFacebookPage)
r.POST("/api/v1/accounts/:account_id/callbacks/facebook_pages", handler.FacebookPages)
r.POST("/api/v1/accounts/:account_id/callbacks/facebook_pages.json", handler.FacebookPages)
r.POST("/api/v1/accounts/:account_id/callbacks/reauthorize_page", handler.ReauthorizePage)
return r, db
}
func TestFacebookCallbacks_RegisterFacebookPage(t *testing.T) {
router, db := setupFacebookCallbackTest(t, &fakeFacebookCallbackProvider{instagramID: "ig-123"})
account := model.Account{Name: "Acme", Active: true}
require.NoError(t, db.Create(&account).Error)
body := map[string]any{"user_access_token": "user-token", "page_access_token": "page-token", "page_id": "page-1", "inbox_name": "Facebook"}
rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/register_facebook_page", body)
require.Equal(t, http.StatusOK, rec.Code)
var payload map[string]any
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
assert.Equal(t, "Facebook", payload["name"])
assert.Equal(t, "facebook", payload["channel_type"])
assert.Equal(t, "page-1", payload["page_id"])
var channel channelmodel.ChannelFacebook
require.NoError(t, db.First(&channel).Error)
assert.Equal(t, account.ID, channel.AccountID)
assert.Equal(t, "user-token", channel.UserAccessToken)
assert.Equal(t, "page-token", channel.PageAccessToken)
assert.Equal(t, "ig-123", channel.InstagramBusinessAccountID)
}
func TestFacebookCallbacks_FacebookPagesMarksExistingPages(t *testing.T) {
provider := &fakeFacebookCallbackProvider{longToken: "long-user-token", pages: []facebookchannel.FBPageInfo{{ID: "page-1", Name: "Existing", AccessToken: "token-1"}, {ID: "page-2", Name: "New", AccessToken: "token-2"}}}
router, db := setupFacebookCallbackTest(t, provider)
account := model.Account{Name: "Acme", Active: true}
require.NoError(t, db.Create(&account).Error)
require.NoError(t, db.Create(&channelmodel.ChannelFacebook{AccountID: account.ID, InboxID: 10, PageID: "page-1", PageAccessToken: "old"}).Error)
rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/facebook_pages.json", map[string]any{"omniauth_token": "short-token"})
require.Equal(t, http.StatusOK, rec.Code)
var payload struct {
Data struct {
UserAccessToken string `json:"user_access_token"`
PageDetails []struct {
ID string `json:"id"`
Exists bool `json:"exists"`
} `json:"page_details"`
} `json:"data"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload))
assert.Equal(t, "long-user-token", payload.Data.UserAccessToken)
require.Len(t, payload.Data.PageDetails, 2)
assert.True(t, payload.Data.PageDetails[0].Exists)
assert.False(t, payload.Data.PageDetails[1].Exists)
}
func TestFacebookCallbacks_ReauthorizePageUpdatesMatchingPage(t *testing.T) {
provider := &fakeFacebookCallbackProvider{longToken: "long-user-token", instagramID: "ig-456", pages: []facebookchannel.FBPageInfo{{ID: "page-1", Name: "Renamed", AccessToken: "new-page-token"}}}
router, db := setupFacebookCallbackTest(t, provider)
account := model.Account{Name: "Acme", Active: true}
require.NoError(t, db.Create(&account).Error)
inbox := createTestChannelInbox(t, db, account.ID, 0, "Facebook", "facebook", map[string]any{"page_id": "page-1", "page_access_token": "old-page-token"})
channel := channelmodel.ChannelFacebook{AccountID: account.ID, InboxID: inbox.ID, PageID: "page-1", PageAccessToken: "old-page-token", ReauthorizationRequired: true}
require.NoError(t, db.Create(&channel).Error)
inbox.ChannelID = channel.ID
require.NoError(t, db.Save(inbox).Error)
rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/reauthorize_page", map[string]any{"omniauth_token": "short-token", "inbox_id": inbox.ID})
require.Equal(t, http.StatusOK, rec.Code)
var updated channelmodel.ChannelFacebook
require.NoError(t, db.First(&updated, channel.ID).Error)
assert.Equal(t, "long-user-token", updated.UserAccessToken)
assert.Equal(t, "new-page-token", updated.PageAccessToken)
assert.Equal(t, "Renamed", updated.PageName)
assert.Equal(t, "ig-456", updated.InstagramBusinessAccountID)
assert.False(t, updated.ReauthorizationRequired)
var updatedInbox model.Inbox
require.NoError(t, db.First(&updatedInbox, inbox.ID).Error)
var config map[string]any
require.NoError(t, json.Unmarshal([]byte(updatedInbox.ChannelConfig), &config))
assert.Equal(t, "new-page-token", config["page_access_token"])
}
func TestFacebookCallbacks_ReauthorizePageReturns422WhenPageMissing(t *testing.T) {
provider := &fakeFacebookCallbackProvider{longToken: "long-user-token", pages: []facebookchannel.FBPageInfo{{ID: "other-page", AccessToken: "token"}}}
router, db := setupFacebookCallbackTest(t, provider)
account := model.Account{Name: "Acme", Active: true}
require.NoError(t, db.Create(&account).Error)
inbox := createTestChannelInbox(t, db, account.ID, 0, "Facebook", "facebook", map[string]any{"page_id": "page-1"})
require.NoError(t, db.Create(&channelmodel.ChannelFacebook{AccountID: account.ID, InboxID: inbox.ID, PageID: "page-1", PageAccessToken: "old"}).Error)
rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/reauthorize_page", map[string]any{"omniauth_token": "short-token", "inbox_id": inbox.ID})
assert.Equal(t, http.StatusUnprocessableEntity, rec.Code)
}
func performFacebookCallbackRequest(t *testing.T, router *gin.Engine, method, path string, body map[string]any) *httptest.ResponseRecorder {
t.Helper()
encoded, err := json.Marshal(body)
require.NoError(t, err)
req := httptest.NewRequest(method, path, bytes.NewReader(encoded))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
return rec
}
@@ -15,23 +15,35 @@ package v1
// - GET /api/v1/accounts/:id/channels/facebook_channel/authorization → OAuth authorize URL
import (
"context"
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
facebookchannel "github.com/gochat/gochat/internal/channel/facebook"
"github.com/gochat/gochat/internal/model"
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"
)
type FacebookCallbackProvider interface {
ExchangeLongLivedUserToken(ctx context.Context, omniauthToken string) (string, error)
ListFacebookPages(ctx context.Context, userAccessToken string) ([]facebookchannel.FBPageInfo, error)
FetchInstagramBusinessAccountID(ctx context.Context, pageAccessToken string) (string, error)
}
// 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
fbCallbacks FacebookCallbackProvider
inboxSvc *service.InboxService
fbRepo *repository.ChannelFacebookRepo
}
@@ -42,10 +54,16 @@ func NewFacebookChannelHandler(
fbProvider *facebookchannel.FacebookProvider,
inboxSvc *service.InboxService,
fbRepo *repository.ChannelFacebookRepo,
callbackProviders ...FacebookCallbackProvider,
) *FacebookChannelHandler {
var callbacks FacebookCallbackProvider = fbProvider
if len(callbackProviders) > 0 && callbackProviders[0] != nil {
callbacks = callbackProviders[0]
}
return &FacebookChannelHandler{
fbChannelSvc: fbChannelSvc,
fbProvider: fbProvider,
fbCallbacks: callbacks,
inboxSvc: inboxSvc,
fbRepo: fbRepo,
}
@@ -61,11 +79,8 @@ type FacebookAuthorizationRequest struct {
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
@@ -80,7 +95,7 @@ func (h *FacebookChannelHandler) Authorization(c *gin.Context) {
req.RedirectURL = redirectURL
}
authURL, err := h.fbProvider.BuildAuthURL(c.Request.Context(), uint(accountID), req.RedirectURL)
authURL, err := h.fbProvider.BuildAuthURL(c.Request.Context(), 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"})
@@ -99,14 +114,164 @@ type FacebookOAuthCallbackRequest struct {
RedirectURL string `json:"redirect_url" validate:"required,url"`
}
type FacebookCallbackRegisterRequest struct {
OmniauthToken string `json:"omniauth_token" form:"omniauth_token"`
UserAccessToken string `json:"user_access_token" form:"user_access_token"`
PageAccessToken string `json:"page_access_token" form:"page_access_token"`
PageID string `json:"page_id" form:"page_id"`
InboxName string `json:"inbox_name" form:"inbox_name"`
PageName string `json:"page_name" form:"page_name"`
EnableAutoAssignment bool `json:"enable_auto_assignment" form:"enable_auto_assignment"`
}
type FacebookCallbackPagesRequest struct {
OmniauthToken string `json:"omniauth_token" form:"omniauth_token"`
}
type FacebookCallbackReauthorizeRequest struct {
OmniauthToken string `json:"omniauth_token" form:"omniauth_token"`
InboxID uint `json:"inbox_id" form:"inbox_id"`
}
// RegisterFacebookPage matches Chatwoot CallbacksController#register_facebook_page.
func (h *FacebookChannelHandler) RegisterFacebookPage(c *gin.Context) {
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
var req FacebookCallbackRegisterRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if strings.TrimSpace(req.UserAccessToken) == "" && strings.TrimSpace(req.OmniauthToken) != "" {
userToken, err := h.fbCallbacks.ExchangeLongLivedUserToken(c.Request.Context(), req.OmniauthToken)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
req.UserAccessToken = userToken
}
inboxName := strings.TrimSpace(req.InboxName)
if inboxName == "" {
inboxName = strings.TrimSpace(req.PageName)
}
if inboxName == "" || strings.TrimSpace(req.PageID) == "" || strings.TrimSpace(req.PageAccessToken) == "" {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "inbox_name, page_id and page_access_token are required"})
return
}
inbox, err := h.inboxSvc.CreateFacebookInbox(c.Request.Context(), accountID, service.CreateFacebookInboxRequest{
Name: inboxName,
PageID: req.PageID,
PageAccessToken: req.PageAccessToken,
UserAccessToken: req.UserAccessToken,
PageName: req.PageName,
EnableAutoAssignment: req.EnableAutoAssignment,
}, h.fbRepo)
if err != nil {
if renderInboxLimitExceeded(c, err) {
return
}
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
h.setFacebookInstagramID(c.Request.Context(), inbox.ChannelID, req.PageAccessToken)
c.JSON(http.StatusOK, h.facebookRegisterPayload(c.Request.Context(), inbox))
}
// FacebookPages matches Chatwoot CallbacksController#facebook_pages.
func (h *FacebookChannelHandler) FacebookPages(c *gin.Context) {
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
var req FacebookCallbackPagesRequest
_ = c.ShouldBind(&req)
userToken, err := h.fbCallbacks.ExchangeLongLivedUserToken(c.Request.Context(), req.OmniauthToken)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
pages, err := h.fbCallbacks.ListFacebookPages(c.Request.Context(), userToken)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
return
}
existing := h.existingFacebookPages(c.Request.Context(), accountID)
pageDetails := make([]gin.H, 0, len(pages))
for _, page := range pages {
pageDetails = append(pageDetails, gin.H{
"id": page.ID,
"name": page.Name,
"access_token": page.AccessToken,
"exists": existing[page.ID],
})
}
c.JSON(http.StatusOK, gin.H{"data": gin.H{"page_details": pageDetails, "user_access_token": userToken}})
}
// ReauthorizePage matches Chatwoot CallbacksController#reauthorize_page.
func (h *FacebookChannelHandler) ReauthorizePage(c *gin.Context) {
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
var req FacebookCallbackReauthorizeRequest
if err := c.ShouldBind(&req); err != nil || req.InboxID == 0 {
c.Status(http.StatusUnprocessableEntity)
return
}
inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), accountID, req.InboxID)
if err != nil || inbox.ChannelType != "facebook" {
c.Status(http.StatusUnprocessableEntity)
return
}
channel, err := h.fbRepo.FindByAccountAndInboxID(c.Request.Context(), accountID, inbox.ID)
if err != nil {
c.Status(http.StatusUnprocessableEntity)
return
}
userToken, err := h.fbCallbacks.ExchangeLongLivedUserToken(c.Request.Context(), req.OmniauthToken)
if err != nil {
c.Status(http.StatusUnprocessableEntity)
return
}
pages, err := h.fbCallbacks.ListFacebookPages(c.Request.Context(), userToken)
if err != nil {
c.Status(http.StatusUnprocessableEntity)
return
}
var matched *facebookchannel.FBPageInfo
for i := range pages {
if pages[i].ID == channel.PageID {
matched = &pages[i]
break
}
}
if matched == nil || matched.AccessToken == "" {
c.Status(http.StatusUnprocessableEntity)
return
}
channel.UserAccessToken = userToken
channel.PageAccessToken = matched.AccessToken
channel.ReauthorizationRequired = false
if matched.Name != "" {
channel.PageName = matched.Name
}
h.setFacebookInstagramIDOnChannel(c.Request.Context(), channel, matched.AccessToken)
if err := h.fbRepo.Update(c.Request.Context(), channel); err != nil {
c.Status(http.StatusUnprocessableEntity)
return
}
h.updateFacebookInboxConfig(c.Request.Context(), inbox, channel)
c.JSON(http.StatusOK, gin.H{"data": serializeInbox(inbox)})
}
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
@@ -146,11 +311,8 @@ type CreateFacebookPageRequest struct {
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
@@ -173,7 +335,7 @@ func (h *FacebookChannelHandler) CreateFacebookPage(c *gin.Context) {
EnableAutoAssignment: req.EnableAutoAssignment,
}
inbox, err := h.inboxSvc.CreateFacebookInbox(ctx, uint(accountID), inboxReq, h.fbRepo)
inbox, err := h.inboxSvc.CreateFacebookInbox(ctx, accountID, inboxReq, h.fbRepo)
if err != nil {
applogger.L().Errorf("Failed to create Facebook inbox: %v", err)
if renderInboxLimitExceeded(c, err) {
@@ -198,10 +360,8 @@ func (h *FacebookChannelHandler) CreateFacebookPage(c *gin.Context) {
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
@@ -222,7 +382,7 @@ func (h *FacebookChannelHandler) GetFacebookChannel(c *gin.Context) {
}
// Verify account ownership
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "Facebook channel does not belong to this account"})
return
}
@@ -241,16 +401,14 @@ func (h *FacebookChannelHandler) GetFacebookChannel(c *gin.Context) {
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
ctx := c.Request.Context()
channels, err := h.fbChannelSvc.ListByAccount(ctx, uint(accountID))
channels, err := h.fbChannelSvc.ListByAccount(ctx, 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"})
@@ -279,10 +437,8 @@ func (h *FacebookChannelHandler) ListFacebookChannels(c *gin.Context) {
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
@@ -302,7 +458,7 @@ func (h *FacebookChannelHandler) DeleteFacebookPage(c *gin.Context) {
}
// Verify account ownership
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "Facebook channel does not belong to this account"})
return
}
@@ -316,7 +472,7 @@ func (h *FacebookChannelHandler) DeleteFacebookPage(c *gin.Context) {
// Delete the associated inbox (if exists)
if ch.InboxID > 0 {
if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil {
if delErr := h.inboxSvc.DeleteByAccount(ctx, accountID, ch.InboxID); delErr != nil {
applogger.L().Warnf("Failed to delete inbox for Facebook channel: %v", delErr)
}
}
@@ -334,10 +490,8 @@ type ReauthorizeFacebookPageRequest struct {
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
@@ -351,7 +505,7 @@ func (h *FacebookChannelHandler) ReauthorizeFacebookPage(c *gin.Context) {
ctx := c.Request.Context()
// Find all Facebook channels for this account and update their tokens
channels, listErr := h.fbChannelSvc.ListByAccount(ctx, uint(accountID))
channels, listErr := h.fbChannelSvc.ListByAccount(ctx, accountID)
if listErr != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Facebook channels"})
return
@@ -378,10 +532,8 @@ func (h *FacebookChannelHandler) GetFacebookPage(c *gin.Context) {
// 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"})
accountID, ok := h.parseFacebookAccountID(c)
if !ok {
return
}
@@ -402,7 +554,7 @@ func (h *FacebookChannelHandler) UpdateFacebookPage(c *gin.Context) {
}
// Verify account ownership
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "Facebook channel does not belong to this account"})
return
}
@@ -421,7 +573,7 @@ func (h *FacebookChannelHandler) UpdateFacebookPage(c *gin.Context) {
ReauthorizationRequired: req.ReauthorizationRequired,
}
updated, err := h.fbChannelSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq)
updated, err := h.fbChannelSvc.Update(ctx, 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"})
@@ -437,3 +589,83 @@ func (h *FacebookChannelHandler) UpdateFacebookPage(c *gin.Context) {
"reauthorization_required": updated.ReauthorizationRequired,
})
}
func (h *FacebookChannelHandler) parseFacebookAccountID(c *gin.Context) (uint, bool) {
accountIDStr := c.Param("account_id")
if accountIDStr == "" {
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 0, false
}
return uint(accountID), true
}
func (h *FacebookChannelHandler) facebookRegisterPayload(ctx context.Context, inbox *model.Inbox) gin.H {
var channel channelmodel.ChannelFacebook
if inbox.ChannelID != 0 {
if ch, err := h.fbRepo.FindByID(ctx, inbox.ChannelID); err == nil {
channel = *ch
}
}
return gin.H{
"id": inbox.ID,
"channel_id": inbox.ChannelID,
"name": inbox.Name,
"channel_type": inbox.ChannelType,
"avatar_url": nil,
"page_id": channel.PageID,
"enable_auto_assignment": inbox.EnableAutoAssignment,
}
}
func (h *FacebookChannelHandler) existingFacebookPages(ctx context.Context, accountID uint) map[string]bool {
existing := map[string]bool{}
channels, err := h.fbChannelSvc.ListByAccount(ctx, accountID)
if err != nil {
return existing
}
for _, channel := range channels {
existing[channel.PageID] = true
}
return existing
}
func (h *FacebookChannelHandler) setFacebookInstagramID(ctx context.Context, channelID uint, pageAccessToken string) {
channel, err := h.fbRepo.FindByID(ctx, channelID)
if err != nil {
return
}
h.setFacebookInstagramIDOnChannel(ctx, channel, pageAccessToken)
_ = h.fbRepo.Update(ctx, channel)
}
func (h *FacebookChannelHandler) setFacebookInstagramIDOnChannel(ctx context.Context, channel *channelmodel.ChannelFacebook, pageAccessToken string) {
if h.fbCallbacks == nil || channel == nil {
return
}
instagramID, err := h.fbCallbacks.FetchInstagramBusinessAccountID(ctx, pageAccessToken)
if err != nil || instagramID == "" {
return
}
channel.InstagramBusinessAccountID = instagramID
}
func (h *FacebookChannelHandler) updateFacebookInboxConfig(ctx context.Context, inbox *model.Inbox, channel *channelmodel.ChannelFacebook) {
config := map[string]any{}
_ = json.Unmarshal([]byte(inbox.ChannelConfig), &config)
config["page_id"] = channel.PageID
config["page_access_token"] = channel.PageAccessToken
config["page_name"] = channel.PageName
config["webhook_verify_token"] = channel.WebhookVerifyToken
encoded, _ := json.Marshal(config)
inbox.ChannelConfig = string(encoded)
updated, err := h.inboxSvc.Update(ctx, inbox.AccountID, inbox.ID, service.UpdateInboxRequest{Channel: config})
if err == nil && updated != nil {
*inbox = *updated
} else {
inbox.ChannelConfig = string(encoded)
}
}