Whatsapp Business enhancements
Some checks failed
CI / Test (1.22) (push) Failing after -22m39s
CI / Test (1.23) (push) Failing after -22m40s
CI / Build (push) Successful in -25m42s
CI / Lint (push) Failing after -25m28s

This commit is contained in:
2025-12-30 11:35:10 +02:00
parent d80a6433b9
commit 147dac9b60
4 changed files with 543 additions and 19 deletions

View File

@@ -68,38 +68,87 @@ func NewClient(cfg config.WhatsAppConfig, eventBus *events.EventBus, mediaConfig
// Connect validates the Business API credentials
func (c *Client) Connect(ctx context.Context) error {
// Validate credentials by making a test request to get phone number details
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
c.config.APIVersion,
c.config.PhoneNumberID)
logging.Info("Validating WhatsApp Business API credentials", "account_id", c.id)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
resp, err := c.httpClient.Do(req)
// Step 1: Validate token and check permissions
tokenInfo, err := c.validateToken(ctx)
if err != nil {
c.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, c.id, err))
return fmt.Errorf("failed to validate credentials: %w", err)
return fmt.Errorf("token validation failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
err := fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
// Log token information
logging.Info("Access token validated",
"account_id", c.id,
"token_type", tokenInfo.Type,
"app", tokenInfo.Application,
"app_id", tokenInfo.AppID,
"expires", c.formatExpiry(tokenInfo.ExpiresAt),
"scopes", strings.Join(tokenInfo.Scopes, ", "))
// Check for required permissions
requiredScopes := []string{"whatsapp_business_management", "whatsapp_business_messaging"}
missingScopes := c.checkMissingScopes(tokenInfo.Scopes, requiredScopes)
if len(missingScopes) > 0 {
err := fmt.Errorf("token missing required permissions: %s", strings.Join(missingScopes, ", "))
logging.Error("Insufficient token permissions",
"account_id", c.id,
"missing_scopes", strings.Join(missingScopes, ", "),
"current_scopes", strings.Join(tokenInfo.Scopes, ", "))
c.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, c.id, err))
return err
}
// Step 2: Get phone number details
phoneDetails, err := c.getPhoneNumberDetails(ctx)
if err != nil {
c.eventBus.Publish(events.WhatsAppPairFailedEvent(ctx, c.id, err))
return fmt.Errorf("failed to get phone number details: %w", err)
}
// Log phone number information
logging.Info("Phone number details retrieved",
"account_id", c.id,
"phone_number_id", phoneDetails.ID,
"display_number", phoneDetails.DisplayPhoneNumber,
"verified_name", phoneDetails.VerifiedName,
"verification_status", phoneDetails.CodeVerificationStatus,
"quality_rating", phoneDetails.QualityRating,
"throughput_level", phoneDetails.Throughput.Level)
// Warn if phone number is not verified
if phoneDetails.CodeVerificationStatus != "VERIFIED" {
logging.Warn("Phone number is not verified - messaging capabilities may be limited",
"account_id", c.id,
"status", phoneDetails.CodeVerificationStatus)
}
// Step 3: Get business account details (if business_account_id is provided)
if c.config.BusinessAccountID != "" {
businessDetails, err := c.getBusinessAccountDetails(ctx)
if err != nil {
logging.Warn("Failed to get business account details (non-critical)",
"account_id", c.id,
"business_account_id", c.config.BusinessAccountID,
"error", err)
} else {
logging.Info("Business account details retrieved",
"account_id", c.id,
"business_account_id", businessDetails.ID,
"business_name", businessDetails.Name,
"timezone_id", businessDetails.TimezoneID)
}
}
c.mu.Lock()
c.connected = true
c.mu.Unlock()
logging.Info("Business API client connected", "account_id", c.id, "phone", c.phoneNumber)
c.eventBus.Publish(events.WhatsAppConnectedEvent(ctx, c.id, c.phoneNumber))
logging.Info("Business API client connected successfully",
"account_id", c.id,
"phone", phoneDetails.DisplayPhoneNumber,
"verified_name", phoneDetails.VerifiedName)
c.eventBus.Publish(events.WhatsAppConnectedEvent(ctx, c.id, phoneDetails.DisplayPhoneNumber))
return nil
}
@@ -340,3 +389,154 @@ func jidToPhoneNumber(jid types.JID) string {
return phone
}
// validateToken validates the access token and returns token information
func (c *Client) validateToken(ctx context.Context) (*TokenDebugData, error) {
url := fmt.Sprintf("https://graph.facebook.com/%s/debug_token?input_token=%s",
c.config.APIVersion,
c.config.AccessToken)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create token validation request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to validate token: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read token validation response: %w", err)
}
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil {
return nil, fmt.Errorf("token validation failed: %s (code: %d)", errResp.Error.Message, errResp.Error.Code)
}
return nil, fmt.Errorf("token validation returned status %d: %s", resp.StatusCode, string(body))
}
var tokenResp TokenDebugResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return nil, fmt.Errorf("failed to parse token validation response: %w", err)
}
if !tokenResp.Data.IsValid {
return nil, fmt.Errorf("access token is invalid or expired")
}
return &tokenResp.Data, nil
}
// getPhoneNumberDetails retrieves details about the phone number
func (c *Client) getPhoneNumberDetails(ctx context.Context) (*PhoneNumberDetails, error) {
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
c.config.APIVersion,
c.config.PhoneNumberID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create phone number details request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get phone number details: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read phone number details response: %w", err)
}
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil {
return nil, fmt.Errorf("API error: %s (code: %d, subcode: %d)",
errResp.Error.Message, errResp.Error.Code, errResp.Error.ErrorSubcode)
}
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var phoneDetails PhoneNumberDetails
if err := json.Unmarshal(body, &phoneDetails); err != nil {
return nil, fmt.Errorf("failed to parse phone number details: %w", err)
}
return &phoneDetails, nil
}
// getBusinessAccountDetails retrieves details about the business account
func (c *Client) getBusinessAccountDetails(ctx context.Context) (*BusinessAccountDetails, error) {
url := fmt.Sprintf("https://graph.facebook.com/%s/%s",
c.config.APIVersion,
c.config.BusinessAccountID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create business account details request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get business account details: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read business account details response: %w", err)
}
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.Unmarshal(body, &errResp); err == nil {
return nil, fmt.Errorf("API error: %s (code: %d)", errResp.Error.Message, errResp.Error.Code)
}
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
}
var businessDetails BusinessAccountDetails
if err := json.Unmarshal(body, &businessDetails); err != nil {
return nil, fmt.Errorf("failed to parse business account details: %w", err)
}
return &businessDetails, nil
}
// checkMissingScopes checks which required scopes are missing from the token
func (c *Client) checkMissingScopes(currentScopes []string, requiredScopes []string) []string {
scopeMap := make(map[string]bool)
for _, scope := range currentScopes {
scopeMap[scope] = true
}
var missing []string
for _, required := range requiredScopes {
if !scopeMap[required] {
missing = append(missing, required)
}
}
return missing
}
// formatExpiry formats the expiry timestamp for logging
func (c *Client) formatExpiry(expiresAt int64) string {
if expiresAt == 0 {
return "never"
}
expiryTime := time.Unix(expiresAt, 0)
return expiryTime.Format("2006-01-02 15:04:05 MST")
}