569 lines
19 KiB
Go
569 lines
19 KiB
Go
package v1
|
|
|
|
// TwitterChannelHandler handles Twitter channel CRUD, OAuth flow, and webhook management.
|
|
// Reference: Chatwoot app/controllers/api/v1/accounts/channels/twitter_controller.rb
|
|
//
|
|
// Chatwoot's TwitterController provides:
|
|
// - create: authorize Twitter account → create inbox
|
|
// - destroy: remove Twitter channel
|
|
// - reauthorize: refresh expired access tokens
|
|
//
|
|
// gochat maps these to:
|
|
// - GET /api/v1/accounts/:id/twitter_channels/authorization → OAuth authorize URL
|
|
// - POST /api/v1/accounts/:id/twitter_channels/oauth_callback → OAuth token exchange + inbox creation
|
|
// - DELETE /api/v1/accounts/:id/twitter_channels/:twitter_id → destroy Twitter inbox
|
|
// - POST /api/v1/accounts/:id/twitter_channels/reauthorize → refresh token
|
|
//
|
|
// Additionally, webhook endpoints for Account Activity API:
|
|
// - GET /api/v1/twitter_webhook/webhook → CRC validation
|
|
// - POST /api/v1/twitter_webhook/webhook → event processing
|
|
//
|
|
// New Chatwoot-style OAuth + Webhook routes (G10):
|
|
// - GET /api/v1/accounts/:id/twitter/oauth → OAuth authorize URL
|
|
// - GET /api/v1/accounts/:id/twitter/callback → OAuth callback (redirect)
|
|
// - POST /api/v1/accounts/:id/twitter/webhooks → Register Twitter webhook
|
|
// - GET /api/v1/accounts/:id/twitter/webhooks → List registered webhooks
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
|
|
twitterchannel "github.com/gochat/gochat/internal/channel/twitter"
|
|
"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"
|
|
"github.com/gochat/gochat/pkg/response"
|
|
)
|
|
|
|
var twitterAuthorizationHTTPClient = http.DefaultClient
|
|
|
|
// TwitterChannelHandler handles Twitter/X channel management.
|
|
type TwitterChannelHandler struct {
|
|
twService *service.ChannelTwitterService
|
|
twProvider *twitterchannel.TwitterProvider
|
|
inboxSvc *service.InboxService
|
|
twRepo *repository.ChannelTwitterRepo
|
|
}
|
|
|
|
// NewTwitterChannelHandler creates a new Twitter channel handler.
|
|
func NewTwitterChannelHandler(
|
|
twService *service.ChannelTwitterService,
|
|
twProvider *twitterchannel.TwitterProvider,
|
|
inboxSvc *service.InboxService,
|
|
twRepo *repository.ChannelTwitterRepo,
|
|
) *TwitterChannelHandler {
|
|
return &TwitterChannelHandler{
|
|
twService: twService,
|
|
twProvider: twProvider,
|
|
inboxSvc: inboxSvc,
|
|
twRepo: twRepo,
|
|
}
|
|
}
|
|
|
|
// === OAuth Authorization ===
|
|
|
|
// TwitterAuthorizationRequest is the DTO for initiating Twitter OAuth flow.
|
|
type TwitterAuthorizationRequest struct {
|
|
RedirectURL string `json:"redirect_url" validate:"required,url"`
|
|
}
|
|
|
|
// Authorization generates a Twitter OAuth 2.0 authorize URL with PKCE.
|
|
// GET /api/v1/accounts/:id/twitter_channels/authorization
|
|
func (h *TwitterChannelHandler) 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 TwitterAuthorizationRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
authURL, err := h.twProvider.BuildAuthURL(c.Request.Context(), uint(accountID), req.RedirectURL)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to build Twitter 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})
|
|
}
|
|
|
|
// ChatwootAuthorization creates a Twitter OAuth1 authorization URL.
|
|
// POST /api/v1/accounts/:account_id/twitter/authorization
|
|
func (h *TwitterChannelHandler) ChatwootAuthorization(c *gin.Context) {
|
|
accountID, err := parseUintParam(c, "account_id")
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
|
return
|
|
}
|
|
|
|
redirectURL, err := buildTwitterChatwootAuthorizationURL(c.Request.Context(), accountID)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to build Twitter authorization URL: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"success": false})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "url": redirectURL})
|
|
}
|
|
|
|
func buildTwitterChatwootAuthorizationURL(ctx context.Context, accountID uint) (string, error) {
|
|
consumerKey := strings.TrimSpace(os.Getenv("TWITTER_CONSUMER_KEY"))
|
|
consumerSecret := strings.TrimSpace(os.Getenv("TWITTER_CONSUMER_SECRET"))
|
|
if consumerKey == "" || consumerSecret == "" {
|
|
return "", fmt.Errorf("Twitter OAuth is not configured")
|
|
}
|
|
|
|
state, err := signedTwitterState(accountID, consumerSecret)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
callbackURL := strings.TrimRight(envOrDefaultV1("FRONTEND_URL", "http://localhost:3000"), "/") + "/twitter/callback?state=" + url.QueryEscape(state)
|
|
requestTokenURL := envOrDefaultV1("TWITTER_OAUTH_REQUEST_TOKEN_URL", "https://api.twitter.com/oauth/request_token")
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestTokenURL, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
authHeader, err := twitterOAuth1Header(http.MethodPost, requestTokenURL, consumerKey, consumerSecret, callbackURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Authorization", authHeader)
|
|
req.Header.Set("Accept", "application/x-www-form-urlencoded")
|
|
|
|
resp, err := twitterAuthorizationHTTPClient.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
return "", fmt.Errorf("twitter request token failed: %s", resp.Status)
|
|
}
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
values, err := url.ParseQuery(string(body))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
oauthToken := strings.TrimSpace(values.Get("oauth_token"))
|
|
if oauthToken == "" {
|
|
return "", fmt.Errorf("twitter request token missing oauth_token")
|
|
}
|
|
authorizeBase := strings.TrimRight(envOrDefaultV1("TWITTER_API_BASE_URL", "https://api.twitter.com"), "/")
|
|
return authorizeBase + "/oauth/authorize?oauth_token=" + url.QueryEscape(oauthToken), nil
|
|
}
|
|
|
|
func signedTwitterState(accountID uint, secret string) (string, error) {
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"sub": accountID,
|
|
"iat": time.Now().Unix(),
|
|
})
|
|
return token.SignedString([]byte(secret))
|
|
}
|
|
|
|
func twitterOAuth1Header(method, rawURL, consumerKey, consumerSecret, callbackURL string) (string, error) {
|
|
nonceBytes := make([]byte, 16)
|
|
if _, err := rand.Read(nonceBytes); err != nil {
|
|
return "", err
|
|
}
|
|
params := map[string]string{
|
|
"oauth_callback": callbackURL,
|
|
"oauth_consumer_key": consumerKey,
|
|
"oauth_nonce": hex.EncodeToString(nonceBytes),
|
|
"oauth_signature_method": "HMAC-SHA1",
|
|
"oauth_timestamp": strconv.FormatInt(time.Now().Unix(), 10),
|
|
"oauth_version": "1.0",
|
|
}
|
|
params["oauth_signature"] = oauth1Signature(method, rawURL, params, consumerSecret)
|
|
|
|
keys := make([]string, 0, len(params))
|
|
for key := range params {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
parts := make([]string, 0, len(keys))
|
|
for _, key := range keys {
|
|
parts = append(parts, fmt.Sprintf(`%s="%s"`, oauthPercentEncode(key), oauthPercentEncode(params[key])))
|
|
}
|
|
return "OAuth " + strings.Join(parts, ", "), nil
|
|
}
|
|
|
|
func oauth1Signature(method, rawURL string, params map[string]string, consumerSecret string) string {
|
|
keys := make([]string, 0, len(params))
|
|
for key := range params {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
encodedParams := make([]string, 0, len(keys))
|
|
for _, key := range keys {
|
|
encodedParams = append(encodedParams, oauthPercentEncode(key)+"="+oauthPercentEncode(params[key]))
|
|
}
|
|
base := strings.ToUpper(method) + "&" + oauthPercentEncode(rawURL) + "&" + oauthPercentEncode(strings.Join(encodedParams, "&"))
|
|
mac := hmac.New(sha1.New, []byte(oauthPercentEncode(consumerSecret)+"&"))
|
|
_, _ = mac.Write([]byte(base))
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func oauthPercentEncode(value string) string {
|
|
return strings.ReplaceAll(url.QueryEscape(value), "+", "%20")
|
|
}
|
|
|
|
func envOrDefaultV1(key string, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// === OAuth Callback ===
|
|
|
|
// TwitterOAuthCallbackRequest is the DTO for Twitter OAuth callback.
|
|
type TwitterOAuthCallbackRequest struct {
|
|
Code string `json:"code" validate:"required"`
|
|
State string `json:"state" validate:"required"`
|
|
RedirectURL string `json:"redirect_url" validate:"required,url"`
|
|
Name string `json:"name" validate:"required,min=2"`
|
|
}
|
|
|
|
// OAuthCallback exchanges the Twitter OAuth 2.0 code for tokens and creates a channel.
|
|
// POST /api/v1/accounts/:id/twitter_channels/oauth_callback
|
|
func (h *TwitterChannelHandler) 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 TwitterOAuthCallbackRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
if err := h.inboxSvc.EnsureCanCreateInbox(ctx, uint(accountID)); err != nil {
|
|
if renderInboxLimitExceeded(c, err) {
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create twitter inbox"})
|
|
return
|
|
}
|
|
|
|
// Exchange the code for tokens (PKCE verifier stored in state)
|
|
// For now, use empty verifier since state-based verifier retrieval requires session storage
|
|
// ExchangeToken interface takes (ctx, code, redirectURL); PKCE is handled internally
|
|
tokenResult, err := h.twProvider.ExchangeToken(ctx, req.Code, req.RedirectURL)
|
|
if err != nil {
|
|
applogger.L().Errorf("Twitter OAuth token exchange failed: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to exchange OAuth token"})
|
|
return
|
|
}
|
|
|
|
// Get the Twitter user info using the access token
|
|
valid, err := h.twProvider.ValidateAccessToken(ctx, tokenResult.AccessToken)
|
|
if err != nil || !valid {
|
|
applogger.L().Errorf("Twitter token validation failed: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to validate Twitter token"})
|
|
return
|
|
}
|
|
|
|
// Create the Twitter channel record
|
|
twChannel := &channelmodel.ChannelTwitter{
|
|
AccountID: uint(accountID),
|
|
AccessToken: tokenResult.AccessToken,
|
|
RefreshToken: tokenResult.RefreshToken,
|
|
WebhookEnv: h.twProvider.GetWebhookEnv(),
|
|
}
|
|
|
|
if err := h.twService.Create(ctx, twChannel); err != nil {
|
|
applogger.L().Errorf("Failed to create Twitter channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create twitter channel"})
|
|
return
|
|
}
|
|
|
|
// Create the inbox
|
|
inboxReq := service.CreateInboxRequest{
|
|
Name: req.Name,
|
|
ChannelType: string(model.InboxChannelTypeTwitter),
|
|
}
|
|
|
|
inbox, err := h.inboxSvc.Create(ctx, uint(accountID), inboxReq)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to create Twitter inbox: %v", err)
|
|
if delErr := h.twService.Delete(ctx, twChannel.ID); delErr != nil {
|
|
applogger.L().Warnf("Failed to rollback Twitter channel after inbox creation failure: %v", delErr)
|
|
}
|
|
if renderInboxLimitExceeded(c, err) {
|
|
return
|
|
}
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create twitter inbox"})
|
|
return
|
|
}
|
|
|
|
// Link the channel to the inbox
|
|
twChannel.InboxID = inbox.ID
|
|
if err := h.twService.Update(ctx, twChannel); err != nil {
|
|
applogger.L().Warnf("Failed to link Twitter channel to inbox: %v", err)
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"inbox": gin.H{
|
|
"id": inbox.ID,
|
|
"name": inbox.Name,
|
|
"channel_type": inbox.ChannelType,
|
|
},
|
|
"twitter_channel": gin.H{
|
|
"id": twChannel.ID,
|
|
"access_token": twChannel.AccessToken,
|
|
"webhook_env": twChannel.WebhookEnv,
|
|
},
|
|
})
|
|
}
|
|
|
|
// === Delete ===
|
|
|
|
// Delete removes a Twitter channel and its associated inbox.
|
|
// DELETE /api/v1/accounts/:id/twitter_channels/:twitter_id
|
|
func (h *TwitterChannelHandler) Delete(c *gin.Context) {
|
|
twitterIDStr := c.Param("twitter_id")
|
|
twitterID, err := strconv.ParseUint(twitterIDStr, 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid twitter_id"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Get channel to find inbox for deletion
|
|
ch, err := h.twService.GetByID(ctx, uint(twitterID))
|
|
if err != nil {
|
|
applogger.L().Errorf("Twitter channel not found: %v", err)
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "twitter channel not found"})
|
|
return
|
|
}
|
|
|
|
// Delete the Twitter webhook if one is registered
|
|
if ch.WebhookID != "" {
|
|
if err := h.twProvider.DeleteWebhook(ctx, ch.AccessToken, ch.WebhookID); err != nil {
|
|
applogger.L().Warnf("Failed to delete Twitter webhook: %v", err)
|
|
}
|
|
}
|
|
|
|
// Delete the Twitter channel record
|
|
if err := h.twService.Delete(ctx, uint(twitterID)); err != nil {
|
|
applogger.L().Errorf("Failed to delete Twitter channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete twitter channel"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "twitter channel deleted"})
|
|
}
|
|
|
|
// === Webhook CRC Validation ===
|
|
|
|
// TwitterWebhookCRCRequest handles Twitter Account Activity CRC validation.
|
|
// GET /api/v1/twitter_webhook/webhook?crc_token=...
|
|
func (h *TwitterChannelHandler) WebhookCRC(c *gin.Context) {
|
|
crcToken := c.Query("crc_token")
|
|
if crcToken == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing crc_token"})
|
|
return
|
|
}
|
|
|
|
responseToken := h.twProvider.ValidateCRC(crcToken)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"response_token": responseToken,
|
|
})
|
|
}
|
|
|
|
// === Webhook Event Processing ===
|
|
|
|
// TwitterWebhookEvent processes incoming Twitter Account Activity events.
|
|
// POST /api/v1/twitter_webhook/webhook
|
|
func (h *TwitterChannelHandler) WebhookEvent(c *gin.Context) {
|
|
var payload map[string]interface{}
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
applogger.L().Infof("Twitter webhook event received")
|
|
// Event processing will be handled by the Twitter incoming message service
|
|
// (to be implemented in a future milestone)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "received"})
|
|
}
|
|
|
|
// === Webhook Registration (G10) ===
|
|
|
|
// TwitterRegisterWebhookRequest is the DTO for registering a Twitter webhook.
|
|
type TwitterRegisterWebhookRequest struct {
|
|
WebhookURL string `json:"webhook_url" validate:"required,url"`
|
|
}
|
|
|
|
// RegisterWebhook registers a webhook URL with the Twitter Account Activity API.
|
|
// POST /api/v1/accounts/:id/twitter/webhooks
|
|
func (h *TwitterChannelHandler) RegisterWebhook(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 TwitterRegisterWebhookRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Find the Twitter channel for this account to get the access token
|
|
channels, err := h.twService.ListByAccount(ctx, uint(accountID))
|
|
if err != nil || len(channels) == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "no twitter channel found for this account"})
|
|
return
|
|
}
|
|
|
|
ch := &channels[0]
|
|
webhookID, err := h.twProvider.RegisterWebhook(ctx, ch.AccessToken, req.WebhookURL)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to register Twitter webhook: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to register twitter webhook"})
|
|
return
|
|
}
|
|
|
|
// Update the channel with the webhook ID
|
|
ch.WebhookID = webhookID
|
|
if err := h.twService.Update(ctx, ch); err != nil {
|
|
applogger.L().Warnf("Failed to update Twitter channel webhook_id: %v", err)
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"webhook_id": webhookID,
|
|
"webhook_url": req.WebhookURL,
|
|
})
|
|
}
|
|
|
|
// ListWebhooks lists all registered webhooks for the Twitter Account Activity API.
|
|
// GET /api/v1/accounts/:id/twitter/webhooks
|
|
func (h *TwitterChannelHandler) ListWebhooks(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.twService.ListByAccount(ctx, uint(accountID))
|
|
if err != nil || len(channels) == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "no twitter channel found for this account"})
|
|
return
|
|
}
|
|
|
|
ch := &channels[0]
|
|
webhooks, err := h.twProvider.ListWebhooks(ctx, ch.AccessToken)
|
|
if err != nil {
|
|
applogger.L().Errorf("Failed to list Twitter webhooks: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list twitter webhooks"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"webhooks": webhooks,
|
|
})
|
|
}
|
|
|
|
// === Chatwoot-style GET OAuth callback (G10) ===
|
|
|
|
// OAuthCallbackGET handles the OAuth redirect callback from Twitter.
|
|
// GET /api/v1/accounts/:id/twitter/callback?code=...&state=...
|
|
// This is the redirect endpoint that the Twitter OAuth provider calls back to.
|
|
func (h *TwitterChannelHandler) OAuthCallbackGET(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
|
|
}
|
|
|
|
code := c.Query("code")
|
|
state := c.Query("state")
|
|
if code == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing code parameter"})
|
|
return
|
|
}
|
|
|
|
ctx := c.Request.Context()
|
|
|
|
// Determine redirect URL from the configured OAuth config
|
|
redirectURL := h.twProvider.GetOAuthRedirectURL()
|
|
|
|
tokenResult, err := h.twProvider.ExchangeToken(ctx, code, redirectURL)
|
|
if err != nil {
|
|
applogger.L().Errorf("Twitter OAuth GET callback token exchange failed: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to exchange OAuth token"})
|
|
return
|
|
}
|
|
|
|
valid, err := h.twProvider.ValidateAccessToken(ctx, tokenResult.AccessToken)
|
|
if err != nil || !valid {
|
|
applogger.L().Errorf("Twitter token validation failed: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to validate Twitter token"})
|
|
return
|
|
}
|
|
|
|
// Create the Twitter channel record
|
|
twChannel := &channelmodel.ChannelTwitter{
|
|
AccountID: uint(accountID),
|
|
AccessToken: tokenResult.AccessToken,
|
|
RefreshToken: tokenResult.RefreshToken,
|
|
WebhookEnv: h.twProvider.GetWebhookEnv(),
|
|
}
|
|
|
|
if err := h.twService.Create(ctx, twChannel); err != nil {
|
|
applogger.L().Errorf("Failed to create Twitter channel: %v", err)
|
|
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create twitter channel"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"channel_id": twChannel.ID,
|
|
"access_token": twChannel.AccessToken,
|
|
"webhook_env": twChannel.WebhookEnv,
|
|
"state": state,
|
|
})
|
|
}
|