1
0
Fork 0
telegraf/plugins/inputs/mailchimp/chimp_api.go
Daniel Baumann 4978089aab
Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-24 07:26:29 +02:00

248 lines
5.7 KiB
Go

package mailchimp
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"sync"
"time"
"github.com/influxdata/telegraf"
)
const (
reportsEndpoint string = "/3.0/reports"
reportsEndpointCampaign string = "/3.0/reports/%s"
)
var mailchimpDatacenter = regexp.MustCompile("[a-z]+[0-9]+$")
type chimpAPI struct {
transport http.RoundTripper
debug bool
sync.Mutex
url *url.URL
log telegraf.Logger
}
type reportsParams struct {
count string
offset string
sinceSendTime string
beforeSendTime string
}
func (p *reportsParams) String() string {
v := url.Values{}
if p.count != "" {
v.Set("count", p.count)
}
if p.offset != "" {
v.Set("offset", p.offset)
}
if p.beforeSendTime != "" {
v.Set("before_send_time", p.beforeSendTime)
}
if p.sinceSendTime != "" {
v.Set("since_send_time", p.sinceSendTime)
}
return v.Encode()
}
func newChimpAPI(apiKey string, log telegraf.Logger) *chimpAPI {
u := &url.URL{}
u.Scheme = "https"
u.Host = mailchimpDatacenter.FindString(apiKey) + ".api.mailchimp.com"
u.User = url.UserPassword("", apiKey)
return &chimpAPI{url: u, log: log}
}
type apiError struct {
Status int `json:"status"`
Type string `json:"type"`
Title string `json:"title"`
Detail string `json:"detail"`
Instance string `json:"instance"`
}
func (e apiError) Error() string {
return fmt.Sprintf("ERROR %v: %v. See %v", e.Status, e.Title, e.Type)
}
func chimpErrorCheck(body []byte) error {
var e apiError
if err := json.Unmarshal(body, &e); err != nil {
return err
}
if e.Title != "" || e.Status != 0 {
return e
}
return nil
}
func (a *chimpAPI) getReports(params reportsParams) (reportsResponse, error) {
a.Lock()
defer a.Unlock()
a.url.Path = reportsEndpoint
var response reportsResponse
rawjson, err := a.runChimp(params)
if err != nil {
return response, err
}
err = json.Unmarshal(rawjson, &response)
if err != nil {
return response, err
}
return response, nil
}
func (a *chimpAPI) getReport(campaignID string) (report, error) {
a.Lock()
defer a.Unlock()
a.url.Path = fmt.Sprintf(reportsEndpointCampaign, campaignID)
var response report
rawjson, err := a.runChimp(reportsParams{})
if err != nil {
return response, err
}
err = json.Unmarshal(rawjson, &response)
if err != nil {
return response, err
}
return response, nil
}
func (a *chimpAPI) runChimp(params reportsParams) ([]byte, error) {
client := &http.Client{
Transport: a.transport,
Timeout: 4 * time.Second,
}
var b bytes.Buffer
req, err := http.NewRequest("GET", a.url.String(), &b)
if err != nil {
return nil, err
}
req.URL.RawQuery = params.String()
req.Header.Set("User-Agent", "Telegraf-MailChimp-Plugin")
if a.debug {
a.log.Debugf("request URL: %s", req.URL.String())
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
//nolint:errcheck // LimitReader returns io.EOF and we're not interested in read errors.
body, _ := io.ReadAll(io.LimitReader(resp.Body, 200))
return nil, fmt.Errorf("%s returned HTTP status %s: %q", a.url.String(), resp.Status, body)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if a.debug {
a.log.Debugf("response Body: %q", string(body))
}
if err := chimpErrorCheck(body); err != nil {
return nil, err
}
return body, nil
}
type reportsResponse struct {
Reports []report `json:"reports"`
TotalItems int `json:"total_items"`
}
type report struct {
ID string `json:"id"`
CampaignTitle string `json:"campaign_title"`
Type string `json:"type"`
EmailsSent int `json:"emails_sent"`
AbuseReports int `json:"abuse_reports"`
Unsubscribed int `json:"unsubscribed"`
SendTime string `json:"send_time"`
TimeSeries []timeSeries
Bounces bounces `json:"bounces"`
Forwards forwards `json:"forwards"`
Opens opens `json:"opens"`
Clicks clicks `json:"clicks"`
FacebookLikes facebookLikes `json:"facebook_likes"`
IndustryStats industryStats `json:"industry_stats"`
ListStats listStats `json:"list_stats"`
}
type bounces struct {
HardBounces int `json:"hard_bounces"`
SoftBounces int `json:"soft_bounces"`
SyntaxErrors int `json:"syntax_errors"`
}
type forwards struct {
ForwardsCount int `json:"forwards_count"`
ForwardsOpens int `json:"forwards_opens"`
}
type opens struct {
OpensTotal int `json:"opens_total"`
UniqueOpens int `json:"unique_opens"`
OpenRate float64 `json:"open_rate"`
LastOpen string `json:"last_open"`
}
type clicks struct {
ClicksTotal int `json:"clicks_total"`
UniqueClicks int `json:"unique_clicks"`
UniqueSubscriberClicks int `json:"unique_subscriber_clicks"`
ClickRate float64 `json:"click_rate"`
LastClick string `json:"last_click"`
}
type facebookLikes struct {
RecipientLikes int `json:"recipient_likes"`
UniqueLikes int `json:"unique_likes"`
FacebookLikes int `json:"facebook_likes"`
}
type industryStats struct {
Type string `json:"type"`
OpenRate float64 `json:"open_rate"`
ClickRate float64 `json:"click_rate"`
BounceRate float64 `json:"bounce_rate"`
UnopenRate float64 `json:"unopen_rate"`
UnsubRate float64 `json:"unsub_rate"`
AbuseRate float64 `json:"abuse_rate"`
}
type listStats struct {
SubRate float64 `json:"sub_rate"`
UnsubRate float64 `json:"unsub_rate"`
OpenRate float64 `json:"open_rate"`
ClickRate float64 `json:"click_rate"`
}
type timeSeries struct {
TimeStamp string `json:"timestamp"`
EmailsSent int `json:"emails_sent"`
UniqueOpens int `json:"unique_opens"`
RecipientsClick int `json:"recipients_click"`
}