feat(channels): align facebook callbacks
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user