Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ jobs:
strategy:
matrix:
os: [Ubuntu]
go-version: ["1.24.x"]
go-version: ["1.25.x"]
runs-on: ${{ matrix.os }}-latest
permissions:
contents: read # for golangci-lint-action
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
lfs: true
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: Install dependencies
Expand All @@ -39,6 +39,6 @@ jobs:
paths: "test-report.xml"
if: always()
- name: Lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v9
with:
version: v2.0.1
version: v2.8.0
21 changes: 14 additions & 7 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
---
version: "2"
linters:
enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2025-02-14 for v1.64.5
enable: # list taken from https://golangci-lint.run/usage/linters/ - last updated 2026-01-09 for v2.8.0
# enabled by default, but list them here to be explicit
- errcheck
- govet
- ineffassign
- staticcheck
- unused
# other linters (that would be disabled by default)
- arangolint # linter for arangoDB
- asasalint # checks for pass []any as any in variadic func(...any)
- asciicheck # checks that your code does not contain non-ASCII identifiers
- bidichk # checks for dangerous unicode character sequences
Expand All @@ -25,6 +26,7 @@ linters:
#- dupl # tool for code clone detection
- dupword # checks for duplicate words in the source code
- durationcheck # checks for two durations multiplied together
- embeddedstructfieldcheck # embedded types should be at the top of a struct
#- err113 # checks the errors handling expressions
- errchkjson # checks types passed to the json encoding functions.
- errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
Expand All @@ -35,6 +37,7 @@ linters:
- fatcontext # finds nested context.WithValue calls in loops
- forbidigo # forbids identifiers
#- forcetypeassert # finds forced type assertions
- funcorder # checks that functions are in the right order
#- funlen # Tool for detection of long functions
#- ginkgolinter # Enforces the Ginkgo testing package guidelines.
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
Expand All @@ -45,6 +48,7 @@ linters:
- goconst # finds repeated strings that could be replaced by a constant
- gocritic # provides diagnostics that check for bugs, performance and style issues
#- gocyclo # Computes and checks the cyclomatic complexity of functions
- godoclint # Checks golang docs best practices (godoc)
#- godot # Check if comments end in a period
#- godox # Tool for detection of FIXME, TODO and other comment keywords
#- goheader # Checks is file header matches to pattern
Expand All @@ -59,6 +63,7 @@ linters:
#- inamedparam # Reports interfaces with unnamed method parameters.
- interfacebloat # Checks the number of methods in an interface
- intrange # finds places where for loops could make use of an integer range
- iotamixing # checks that iota is not mixed with other constants
#- ireturn # Accept Interfaces, Return Concrete Types
#- lll # Reports long lines
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
Expand All @@ -67,20 +72,21 @@ linters:
- mirror # reports wrong mirror patterns of bytes/strings usage
- misspell # finds commonly misspelled English words in comments
#- mnd # Detects magic numbers
- modernize # reports wrong mirror patterns of bytes/strings usage
- musttag # enforces field tags in (un)marshaled structs
- nakedret # finds naked returns in functions greater than a specified function length
#- nestif # Reports deeply nested if statements
- nilerr # finds the code that returns nil even if it checks that the error is not nil
- nilnesserr # Reports constructs that checks for err != nil, but returns a different nil value error.
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
#- nlreturn # Checks for a new line before return and branch statements to increase code clarity
#- noinlineerr # Disallows inline error handling
- noctx # finds sending http request without context.Context
- nolintlint # reports ill-formed or insufficient nolint directives
- nonamedreturns # reports all named returns
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
- perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
#- paralleltest # detects missing usage of t.Parallel() method in your Go test
- perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative.
- perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
- prealloc # finds slice declarations that could potentially be preallocated
- predeclared # finds code that shadows one of Go's predeclared identifiers
- promlinter # checks Prometheus metrics naming via promlint
Expand All @@ -101,15 +107,16 @@ linters:
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
- unconvert # removes unnecessary type conversions
- unparam # reports unused function parameters
- unqueryvet # sql linter disallowing select *
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
- usetesting # reports uses of functions with replacement inside the testing package
#- varnamelen # checks that the length of a variable's name matches its scope
- wastedassign # finds wasted assignment statements
- whitespace # detects leading and trailing whitespace
#- wrapcheck # Checks that errors returned from external packages are wrapped
#- wsl # whitespace linter - add or remove empty lines
#- wsl # (deprecated)
#- wsl_v5 # whitespace linter - add or remove empty lines
#- zerologlint # checks wrong usage of zerolog
#- tenv # deprecated in favor of usetesting
settings:
gosec:
excludes:
Expand Down Expand Up @@ -147,7 +154,7 @@ linters:
strict: false
exclusions:
generated: strict
warn-unused: true
warn-unused: false
presets:
- comments
- common-false-positives
Expand All @@ -164,4 +171,4 @@ formatters:
- gci
- gofmt
- gofumpt
- goimports
- goimports
51 changes: 39 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,6 @@ func main() {

### Contacts

**Find a contact**
```go
contact, err := client.FindContact(ctx, &loops.ContactIdentifier{
Email: loops.String("neil.armstrong@moon.space"),
})
if err != nil {
slog.Error("failed to find contact", slog.Any("error", err.Error()))
return
}
```

**Create a contact**
```go
contactID, err := client.CreateContact(ctx, &loops.Contact{
Expand All @@ -60,7 +49,7 @@ contactID, err := client.CreateContact(ctx, &loops.Contact{
UserGroup: loops.String("Astronauts"),
Subscribed: true,
// custom user defined properties for contacts
CustomProperties: map[string]interface{}{
Properties: map[string]interface{}{
"role": "Astronaut",
},
})
Expand All @@ -70,6 +59,17 @@ if err != nil {
}
```

**Find a contact**
```go
contact, err := client.FindContact(ctx, &loops.ContactIdentifier{
Email: loops.String("neil.armstrong@moon.space"),
})
if err != nil {
slog.Error("failed to find contact", slog.Any("error", err.Error()))
return
}
```

**Delete a contact**
```go
err = client.DeleteContact(ctx, &loops.ContactIdentifier{
Expand All @@ -81,6 +81,17 @@ if err != nil {
}
```

**List contact properties**
```go
properties, err := client.GetContactProperties(ctx, loops.ContactPropertyListOptions{
List: loops.ContactPropertyTypeCustom, // only return your teams custom properties
})
if err != nil {
slog.Error("failed to get contact properties", slog.Any("error", err.Error()))
return
}
```

### Events

**Send an event**
Expand Down Expand Up @@ -116,6 +127,22 @@ if err != nil {
}
```

**List transactional emails**

```go
emailsPage, err := client.ListTransactionalEmails(ctx, loops.ListTransactionalEmailsOptions{
PerPage: 10,
})
if err != nil {
slog.Error("failed to list transactional emails", slog.Any("error", err.Error()))
return
}

for _, email := range emailsPage.Data {
slog.Info("transactional email", slog.String("id", email.ID), slog.String("name", email.Name))
}
```

## API Documentation

The API documentation is part of the official Loops Documentation and can be found [here](https://app.loops.so/docs/api-reference/).
Expand Down
84 changes: 80 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net/http"
"net/url"
"strconv"
)

const defaultURL = "https://app.loops.so/api/v1/"
Expand Down Expand Up @@ -220,17 +221,92 @@ func (c *Client) SendTransactionalEmail(ctx context.Context, transactional *Tran
return err
}

// GetCustomFields retrieves a list of an account's custom contact properties.
func (c *Client) GetCustomFields(ctx context.Context) ([]*CustomField, error) {
type ContactPropertyType int

const (
ContactPropertyTypeAll ContactPropertyType = iota
ContactPropertyTypeCustom
)

type ContactPropertyListOptions struct {
// Which contact properties to return (all or custom to only list your team's custom properties)
List ContactPropertyType
}

// GetContactProperties retrieves a list of an account's contact properties.
// Use listType "all" (default) or "custom" to filter properties.
// See: https://loops.so/docs/api-reference/list-contact-properties
func (c *Client) GetContactProperties(ctx context.Context, opts ContactPropertyListOptions) ([]*ContactProperty, error) {
params := url.Values{}
if opts.List == ContactPropertyTypeCustom {
params.Add("list", "custom")
} else if opts.List != ContactPropertyTypeAll {
return nil, errors.New("invalid list type")
}
req, err := newGetRequestWithQueryParams(c, ctx, "/contacts/properties", params)
if err != nil {
return nil, err
}
return sendRequest[[]*ContactProperty](c, req)
}

// CreateContactProperty creates a new contact property.
// See: https://loops.so/docs/api-reference/create-contact-property
func (c *Client) CreateContactProperty(ctx context.Context, property *ContactPropertyCreate) error {
req, err := newRequestWithBody(c, ctx, http.MethodPost, "/contacts/properties", property)
if err != nil {
return err
}
_, err = sendRequest[*SuccessResponse](c, req)
return err
}

// Deprecated: Use GetContactProperties instead.
func (c *Client) GetCustomFields(ctx context.Context) ([]*ContactProperty, error) {
req, err := newGetRequestWithQueryParams(c, ctx, "/contacts/customFields", nil)
if err != nil {
return nil, err
}
customFields, err := sendRequest[[]*CustomField](c, req)
return sendRequest[[]*ContactProperty](c, req)
}

// GetDedicatedSendingIPs retrieves a list of Loops' dedicated sending IP addresses.
// See: https://loops.so/docs/api-reference/list-dedicated-sending-ips
func (c *Client) GetDedicatedSendingIPs(ctx context.Context) ([]string, error) {
req, err := newGetRequestWithQueryParams(c, ctx, "/dedicated-sending-ips", nil)
if err != nil {
return nil, err
}
return sendRequest[[]string](c, req)
}

type ListTransactionalEmailsOptions struct {
// Number of results per page (10-50, default 20)
PerPage int
// Pagination cursor from previous response
Cursor string
}

// ListTransactionalEmails retrieves a list of published transactional emails.
// perPage: number of results per page (10-50, default 20)
// cursor: pagination cursor from previous response
// See: https://loops.so/docs/api-reference/list-transactional-emails
func (c *Client) ListTransactionalEmails(ctx context.Context, opts ListTransactionalEmailsOptions) (*TransactionalEmailList, error) {
params := url.Values{}
if opts.PerPage != 0 {
if opts.PerPage < 10 || opts.PerPage > 50 {
return nil, errors.New("perPage must be between 10 and 50 (inclusive)")
}
params.Add("perPage", strconv.Itoa(opts.PerPage))
}
if opts.Cursor != "" {
params.Add("cursor", opts.Cursor)
}
req, err := newGetRequestWithQueryParams(c, ctx, "/transactional", params)
if err != nil {
return nil, err
}
return customFields, nil
return sendRequest[*TransactionalEmailList](c, req)
}

// TestAPIKey tests that an API key is valid.
Expand Down
Loading
Loading