Head of Product & Engineering 12c547d215
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix: address remaining golangci-lint warnings
- internal/ghl/oauth.go: acknowledge fmt.Fprint return (errcheck)
- internal/ghl/api.go: handle io.ReadAll error instead of discarding (errcheck)
- internal/cast/client.go: replace defer-in-loop with explicit Body.Close
  after ReadAll (gocritic defer-in-loop)
- internal/phone/normalize.go: move inline regexp.MustCompile to package-level
  var e164Pattern (gocritic / performance)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-05 21:33:03 +02:00

106 lines
2.5 KiB
Go

package cast
import (
"bytes"
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"strconv"
"time"
)
type Client struct {
baseURL string
apiKey string
senderID string
httpClient *http.Client
}
func NewClient(baseURL, apiKey, senderID string) *Client {
return &Client{
baseURL: baseURL,
apiKey: apiKey,
senderID: senderID,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
func (c *Client) SendSMS(ctx context.Context, to, message string) (*SendResponse, error) {
req := SendRequest{To: to, Message: message}
if c.senderID != "" {
req.SenderID = c.senderID
}
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
const maxRetries = 3
backoff := []time.Duration{1 * time.Second, 2 * time.Second, 4 * time.Second}
for attempt := 0; attempt <= maxRetries; attempt++ {
resp, err := c.doRequest(ctx, body)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusTooManyRequests {
if attempt == maxRetries {
return nil, &CastAPIError{StatusCode: resp.StatusCode, APIError: "rate limited, max retries exceeded"}
}
wait := backoff[attempt]
if ra := resp.Header.Get("Retry-After"); ra != "" {
if secs, err := strconv.ParseFloat(ra, 64); err == nil {
wait = time.Duration(secs * float64(time.Second))
}
}
slog.Warn("cast api rate limited, retrying", "attempt", attempt+1, "wait", wait)
resp.Body.Close()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(wait):
}
continue
}
data, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
var errResp SendResponse
_ = json.Unmarshal(data, &errResp)
return nil, &CastAPIError{StatusCode: resp.StatusCode, APIError: errResp.Error}
}
var result SendResponse
if err := json.Unmarshal(data, &result); err != nil {
return nil, err
}
if !result.Success {
return nil, &CastAPIError{StatusCode: resp.StatusCode, APIError: result.Error}
}
return &result, nil
}
return nil, &CastAPIError{StatusCode: http.StatusTooManyRequests, APIError: "max retries exceeded"}
}
func (c *Client) doRequest(ctx context.Context, body []byte) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/sms/send", bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", c.apiKey)
req.Header.Set("Content-Type", "application/json")
return c.httpClient.Do(req)
}