Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,671 @@
|
||||
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 (
|
||||
"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
|
||||
}
|
||||
|
||||
// NewFacebookChannelHandler creates a new Facebook channel handler.
|
||||
func NewFacebookChannelHandler(
|
||||
fbChannelSvc *service.ChannelFacebookService,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// === 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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
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(), 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"`
|
||||
}
|
||||
|
||||
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, nil, inboxSerializationAdmin(c))})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
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, accountID, inboxReq, h.fbRepo)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Failed to create Facebook inbox: %v", err)
|
||||
if renderInboxLimitExceeded(c, err) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
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 != 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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
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"})
|
||||
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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
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 != 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, 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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
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, 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) {
|
||||
accountID, ok := h.parseFacebookAccountID(c)
|
||||
if !ok {
|
||||
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 != 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, 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,
|
||||
})
|
||||
}
|
||||
|
||||
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