package businessapi import ( "bytes" "context" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/url" ) // graphAPIGet performs an authenticated GET to the Graph API and unmarshals the response. func (c *Client) graphAPIGet(ctx context.Context, path string, params url.Values, result any) error { u := fmt.Sprintf("https://graph.facebook.com/%s/%s", c.config.APIVersion, path) if len(params) > 0 { u += "?" + params.Encode() } req, err := http.NewRequestWithContext(ctx, "GET", u, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) return c.executeRequest(req, result) } // graphAPIPost performs an authenticated POST with a JSON body. // body may be nil for action-only endpoints (e.g. publish a flow). func (c *Client) graphAPIPost(ctx context.Context, path string, body any, result any) error { u := fmt.Sprintf("https://graph.facebook.com/%s/%s", c.config.APIVersion, path) var reqBody io.Reader if body != nil { jsonData, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) } reqBody = bytes.NewBuffer(jsonData) } req, err := http.NewRequestWithContext(ctx, "POST", u, reqBody) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) if body != nil { req.Header.Set("Content-Type", "application/json") } return c.executeRequest(req, result) } // graphAPIPostForm performs an authenticated POST with multipart form fields. func (c *Client) graphAPIPostForm(ctx context.Context, path string, fields map[string]string, result any) error { u := fmt.Sprintf("https://graph.facebook.com/%s/%s", c.config.APIVersion, path) var buf bytes.Buffer writer := multipart.NewWriter(&buf) for k, v := range fields { if err := writer.WriteField(k, v); err != nil { return fmt.Errorf("failed to write form field %s: %w", k, err) } } if err := writer.Close(); err != nil { return fmt.Errorf("failed to close multipart writer: %w", err) } req, err := http.NewRequestWithContext(ctx, "POST", u, &buf) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) req.Header.Set("Content-Type", writer.FormDataContentType()) return c.executeRequest(req, result) } // graphAPIDelete performs an authenticated DELETE request. func (c *Client) graphAPIDelete(ctx context.Context, path string, params url.Values) error { u := fmt.Sprintf("https://graph.facebook.com/%s/%s", c.config.APIVersion, path) if len(params) > 0 { u += "?" + params.Encode() } req, err := http.NewRequestWithContext(ctx, "DELETE", u, nil) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) return c.executeRequest(req, nil) } // executeRequest runs an HTTP request, handles error responses, and optionally unmarshals the body. func (c *Client) executeRequest(req *http.Request, result any) error { resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { var errResp ErrorResponse if err := json.Unmarshal(body, &errResp); err == nil && errResp.Error.Message != "" { return fmt.Errorf("API error: %s (code: %d)", errResp.Error.Message, errResp.Error.Code) } return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body)) } if result != nil && len(body) > 0 { if err := json.Unmarshal(body, result); err != nil { return fmt.Errorf("failed to parse response: %w", err) } } return nil } // postToMessagesEndpoint POSTs an arbitrary body to the phone number's /messages endpoint. // Used by sendMessage (typed) and by reaction/read-receipt (different top-level shape). func (c *Client) postToMessagesEndpoint(ctx context.Context, body any) (string, error) { path := c.config.PhoneNumberID + "/messages" jsonData, err := json.Marshal(body) if err != nil { return "", fmt.Errorf("failed to marshal message: %w", err) } u := fmt.Sprintf("https://graph.facebook.com/%s/%s", c.config.APIVersion, path) req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewBuffer(jsonData)) if err != nil { return "", fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return "", fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode != http.StatusOK { var errResp ErrorResponse if err := json.Unmarshal(respBody, &errResp); err == nil { return "", fmt.Errorf("API error: %s (code: %d)", errResp.Error.Message, errResp.Error.Code) } return "", fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBody)) } var sendResp SendMessageResponse if err := json.Unmarshal(respBody, &sendResp); err != nil { return "", nil // some endpoints (read receipt) return non-message JSON } if len(sendResp.Messages) == 0 { return "", nil } return sendResp.Messages[0].ID, nil }