A => LICENSE +32 -0
@@ 0,0 1,32 @@
+Copyright (c) 2022, Netlandish <hello@netlandish.com>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or
+without modification, are permitted provided that the
+following conditions are met:
+
+ * Redistributions of source code must retain the above
+ copyright notice, this list of conditions and the
+ following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials
+ provided with the distribution.
+
+ * Neither the name of Peter Sanchez nor the names of its
+ contributors may be used to endorse or promote products
+ derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
A => client.go +285 -0
@@ 0,0 1,285 @@
+package sendy
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "strconv"
+)
+
+// ErrAPIResponse APIError generic error type
+var ErrAPIResponse = errors.New("An API error occurred")
+
+const (
+ subscribeEndPoint = "/subscribe"
+ unsubscribeEndPoint = "/unsubscribe"
+ subscriptionStatusEndPoint = "/api/subscribers/subscription-status.php"
+ deleteSubscriberEndPoint = "/api/subscribers/delete.php"
+ subscriberCountEndPoint = "/api/subscribers/active-subscriber-count.php"
+ createCampaignEndPoint = "/api/campaigns/create.php"
+ getListEndPoint = "/api/lists/get-lists.php"
+ getBrandsEndPoint = "/api/brands/get-brands.php"
+)
+
+// Given an API Http Response read it and return a string
+func decodeRespose(resp *http.Response) (string, error) {
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return "", err
+ }
+ return string(body), nil
+}
+
+// Given an API Http Response, read it, parse the json and
+// returns a map of Object{ID: string, Name: string}
+func parseJSON(resp *http.Response, errMsgs []string) (map[string]Object, error) {
+ out, err := decodeRespose(resp)
+ if err != nil {
+ return nil, err
+ }
+ for _, v := range errMsgs {
+ if v == out {
+ return nil, fmt.Errorf("%w: %s", ErrAPIResponse, out)
+ }
+ }
+ var objects map[string]Object
+ err = json.Unmarshal([]byte(out), &objects)
+ if err != nil {
+ return nil, err
+ }
+ return objects, nil
+}
+
+// Object is a generic representation for Brand and List
+type Object struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+}
+
+// httpClient defines an interface for http Clients that must implement
+// the PostForm func
+type httpClient interface {
+ PostForm(url string, data url.Values) (*http.Response, error)
+}
+
+// Client defines the API host, API Key and the http Client to use,
+// by default it is a http.Client instance, but you can change it
+// using the SetDefault method
+type Client struct {
+ host string
+ apiKey string
+ httpClient httpClient
+ debug bool
+}
+
+// SetClient changes the http Client used for sending requests to the API
+// Changing this might be useful for test purposes or using a proxy.
+// By default it is an instance of http.Client. The new client must
+// fulfill the `httpClient` interface that implements a PostForm func
+func (c *Client) SetClient(newClient httpClient) {
+ c.httpClient = newClient
+}
+
+// NewClient return a new Client "instance"
+func NewClient(host, apiKey string, debug bool) *Client {
+ return &Client{
+ host: host,
+ apiKey: apiKey,
+ httpClient: &http.Client{},
+ debug: debug,
+ }
+}
+
+// Call is a simple wrapper for client logic. It is used internally by all the endpoint
+// func and get the request params and return a http.Response or an error.
+// The params are:
+// endpoind: API endpoint
+// params: a url.Values map of the EP params
+// required: a string slice with the required params for this EP
+func (c *Client) Call(endpoint string, params url.Values, required []string) (*http.Response, error) {
+ path, err := url.JoinPath(c.host, endpoint)
+ if err != nil {
+ return nil, err
+ }
+ if err = ValidateParams(params, required); err != nil {
+ return nil, err
+ }
+ params.Set("api_key", c.apiKey)
+ params.Set("boolean", "true")
+ if c.debug {
+ log.Printf("Sending: %s\nParams: %s", path, params)
+ }
+ return c.httpClient.PostForm(path, params)
+}
+
+// Subscribe endpoint. It returns an error or nil if the request was sucessful
+// It reicives as params a SubscribeParam struct where Email and List are required.
+// Optional fields are: Name, Country, IPAddress, Referrer, Silent, GDPR and HP
+func (c *Client) Subscribe(p SusbcribeParams) error {
+ resp, err := c.Call(subscribeEndPoint, p.buildParams(), []string{"email", "list"})
+ if err != nil {
+ return err
+ }
+ out, err := decodeRespose(resp)
+ if err != nil {
+ return err
+ }
+ if out != "1" {
+ return fmt.Errorf("%w: %s", ErrAPIResponse, out)
+ }
+ return nil
+}
+
+// Unsubscribe endpoint. It returns an error or nil if the request was sucessful
+// It reicives as params a UnsusbcribeParams struct where Email and List are required.
+func (c *Client) Unsubscribe(p UnsusbcribeParams) error {
+ resp, err := c.Call(unsubscribeEndPoint, p.buildParams(), []string{"email", "list"})
+ if err != nil {
+ return err
+ }
+ out, err := decodeRespose(resp)
+ if err != nil {
+ return err
+ }
+ if out != "1" {
+ return fmt.Errorf("%w: %s", ErrAPIResponse, out)
+ }
+ return nil
+}
+
+// DeleteSubscriber endpoint. It returns an error or nil if the request was sucessful
+// It reicives as params a ActionParams struct where Email and List are required.
+func (c *Client) DeleteSubscriber(p ActionParams) error {
+ resp, err := c.Call(deleteSubscriberEndPoint, p.buildParams(), []string{"list_id", "email"})
+ if err != nil {
+ return err
+ }
+ out, err := decodeRespose(resp)
+ if err != nil {
+ return err
+ }
+ if out != "1" {
+ return fmt.Errorf("%w: %s", ErrAPIResponse, out)
+ }
+ return nil
+}
+
+// SubscriptionStatus endpoint. If success it returns a string describing the status, otherwise an error.
+// Status might be `Subscribed`, `Unsubscribed`, `Unconfirmed`, `Bounced`, `Soft bounced` or `Complained`
+// It reicives as params a ActionParams struct where Email and List are required.
+func (c *Client) SubscriptionStatus(p ActionParams) (string, error) {
+ resp, err := c.Call(subscriptionStatusEndPoint, p.buildParams(), []string{"list_id", "email"})
+ if err != nil {
+ return "", err
+ }
+ out, err := decodeRespose(resp)
+ if err != nil {
+ return "", err
+ }
+ states := []string{
+ "Subscribed", "Unsubscribed", "Unconfirmed", "Bounced",
+ "Soft bounced", "Complained",
+ }
+
+ for _, v := range states {
+ if out == v {
+ return out, nil
+ }
+ }
+ return "", fmt.Errorf("%w: %s", ErrAPIResponse, out)
+}
+
+// SubscriberCount endpoint. If success it returns a string describing the subscriber number, otherwise an error.
+// It reicives as params a ActionParams struct where List is required
+func (c *Client) SubscriberCount(p ActionParams) (string, error) {
+ resp, err := c.Call(subscriberCountEndPoint, p.buildParams(), []string{"list_id"})
+ if err != nil {
+ return "", err
+ }
+ out, err := decodeRespose(resp)
+ if err != nil {
+ return "", err
+ }
+ _, err = strconv.Atoi(out)
+ if err != nil {
+ return "", fmt.Errorf("%w: %s", ErrAPIResponse, out)
+ }
+ return out, nil
+}
+
+// CreateCampaign endpoint. If succes it returns a string describing the action.
+// It might be `Campaign created`, `Campaign created and now sending` or `Campaign scheduled`.
+// Otherwise it returns an error.
+// It reicives as params a CreateCampaignParams struct. The following fields are required
+// FromName, FromEmail, ReplyTo, Title, Subject, HTMLText, TraskOpens, TrackClicks,
+// ScheduleDateTime, ScheduleTimeZone.
+func (c *Client) CreateCampaign(p CreateCampaignParams) (string, error) {
+ required := []string{
+ "from_name", "from_email", "reply_to", "title",
+ "subject", "html_text", "track_opens", "track_clicks",
+ "schedule_date_time", "schedule_timezone",
+ }
+ resp, err := c.Call(createCampaignEndPoint, p.buildParams(), required)
+ if err != nil {
+ return "", err
+ }
+ out, err := decodeRespose(resp)
+ if err != nil {
+ return "", err
+ }
+
+ states := []string{
+ "Campaign created", "Campaign created and now sending",
+ "Campaign scheduled",
+ }
+
+ for _, v := range states {
+ if out == v {
+ return out, nil
+ }
+ }
+ return "", fmt.Errorf("%w: %s", ErrAPIResponse, out)
+}
+
+// GetBrands endpoint. If succeed returns a map of Object of the form
+// map["brand1"]Object{ID: string, Name: string}
+// Otherwhise it returns an error
+func (c *Client) GetBrands() (map[string]Object, error) {
+ resp, err := c.Call(getBrandsEndPoint, url.Values{}, []string{})
+ if err != nil {
+ return nil, err
+ }
+ errMsgs := []string{
+ "No data passed", "API key not passed",
+ "Invalid API key", "No brands found",
+ }
+
+ return parseJSON(resp, errMsgs)
+}
+
+// GetLists endpoint. If succeed returns a map of Object of the form
+// map["brand1"]Object{ID: string, Name: string}
+// Otherwhise it returns an error
+//
+// Params
+// BrandID: the id of the brand you want to get the list of lists from.
+// IncludeHidden (optional): if you want to retrieve lists that are
+// hidden as well, set this to yes. Default is no.
+func (c *Client) GetLists(p GetListsParams) (map[string]Object, error) {
+ resp, err := c.Call(getListEndPoint, p.buildParams(), []string{"brand_id"})
+ if err != nil {
+ return nil, err
+ }
+
+ errMsgs := []string{
+ "No data passed", "API key not passed",
+ "Invalid API key", "Brand ID not passed",
+ "Brand does not exist", "No lists found",
+ }
+ return parseJSON(resp, errMsgs)
+}
A => client_test.go +337 -0
@@ 0,0 1,337 @@
+package sendy_test
+
+import (
+ "bytes"
+ "errors"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "testing"
+
+ sendy "hg.code.netlandish.com/~netlandish/sendygo"
+)
+
+var GetPostFormFunc func(url string, data url.Values) (*http.Response, error)
+
+type MockClient struct{}
+
+func (c MockClient) PostForm(url string, data url.Values) (*http.Response, error) {
+ return GetPostFormFunc(url, data)
+}
+
+func createMockResponse(text string) *http.Response {
+ return &http.Response{
+ Status: "200 OK", StatusCode: http.StatusOK,
+ Body: ioutil.NopCloser(bytes.NewBufferString(text)),
+ }
+}
+
+func TestSubscribeSuccess(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("1"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ err := client.Subscribe(sendy.SusbcribeParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ Name: "Yader Test",
+ Country: "NI",
+ Referrer: "https://waxbar.co",
+ GDPR: true,
+ })
+
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+}
+
+func TestSubscribeError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("Already subscribed."), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ err := client.Subscribe(sendy.SusbcribeParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ Name: "Yader Test",
+ Country: "NI",
+ Referrer: "https://waxbar.co",
+ GDPR: true,
+ })
+
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
+
+func TestUnsubscribeSuccess(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("1"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ err := client.Unsubscribe(sendy.UnsusbcribeParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ })
+
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+}
+
+func TestUnsubscribeError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("Invalid email address."), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ err := client.Unsubscribe(sendy.UnsusbcribeParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ })
+
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
+
+func TestDeleteSubscriberSuccess(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("1"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ err := client.DeleteSubscriber(sendy.ActionParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ })
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+}
+
+func TestDeleteSubscriberError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("Subscriber does not exist"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ err := client.DeleteSubscriber(sendy.ActionParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ })
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
+
+func TestSubscriptionStatusSuccess(t *testing.T) {
+ expected := "Subscribed"
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse(expected), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ out, err := client.SubscriptionStatus(sendy.ActionParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ })
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+
+ if out != expected {
+ t.Errorf("Expected %s, got %s", expected, out)
+ }
+}
+
+func TestSubscriptionStatusError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("No data passed"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ _, err := client.SubscriptionStatus(sendy.ActionParams{
+ List: "xxx",
+ Email: "yader+test7@netlandish.com",
+ })
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
+
+func TestSubscriberCountSuccess(t *testing.T) {
+ expected := "100"
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse(expected), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ out, err := client.SubscriberCount(sendy.ActionParams{
+ List: "xxx",
+ })
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+
+ if out != expected {
+ t.Errorf("Expected %s, got %s", expected, out)
+ }
+}
+
+func TestSubscriberCountError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("Invalid API key"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+
+ _, err := client.SubscriberCount(sendy.ActionParams{
+ List: "xxx",
+ })
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
+
+func TestCreateCampaignSuccess(t *testing.T) {
+ expected := "Campaign scheduled"
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse(expected), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+ out, err := client.CreateCampaign(sendy.CreateCampaignParams{
+ FromName: "Test Campaign 2",
+ FromEmail: "yader+test10@netlandish.com",
+ ReplyTo: "yader+test10@netlandish.com",
+ Title: "Email Title",
+ Subject: "Email Subject",
+ PlainText: "Plain text for the email",
+ HTMLText: "<p>Html format for the email</p>",
+ ListIDS: "xxxx",
+ QueryString: "foo",
+ SendCampaign: 1,
+ TrackOpens: 1,
+ TrackClicks: 1,
+ ScheduleDateTime: "June 15, 2023 6:05pm",
+ ScheduleTimeZone: "America/Managua",
+ })
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+
+ if out != expected {
+ t.Errorf("Expected %s, got %s", expected, out)
+ }
+}
+
+func TestCreateCampaignError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("Unable to create campaign"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+ _, err := client.CreateCampaign(sendy.CreateCampaignParams{
+ FromName: "Test Campaign 2",
+ FromEmail: "yader+test10@netlandish.com",
+ ReplyTo: "yader+test10@netlandish.com",
+ Title: "Email Title",
+ Subject: "Email Subject",
+ PlainText: "Plain text for the email",
+ HTMLText: "<p>Html format for the email</p>",
+ ListIDS: "xxxx",
+ QueryString: "foo",
+ SendCampaign: 1,
+ TrackOpens: 1,
+ TrackClicks: 1,
+ ScheduleDateTime: "June 15, 2023 6:05pm",
+ ScheduleTimeZone: "America/Managua",
+ })
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
+
+func TestGetBrands(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ out := "{\"brand1\": {\"id\": \"4\", \"name\": \"Netlandish Inc.\"}, \"brand2\": {\"id\": \"2\", \"name\": \"Netlandish 2\"}}"
+ return createMockResponse(out), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+ outMap, err := client.GetBrands()
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+
+ if _, ok := outMap["brand1"]; !ok {
+ t.Errorf("brand1 is expected in the parsed map")
+ }
+
+ if outMap["brand1"].ID != "4" || outMap["brand1"].Name != "Netlandish Inc." {
+ t.Errorf("A Brand struct with ID: \"4\" and Name: \"Netlandish Inc.\" is expected")
+ }
+
+}
+
+func TestGetBrandsError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("No brands found"), nil
+ }
+
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+ _, err := client.GetBrands()
+
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
+
+func TestGetLists(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ out := "{\"list1\": {\"id\": \"xyz\", \"name\": \"Netlandish Inc.\"}}"
+ return createMockResponse(out), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+ outMap, err := client.GetLists(sendy.GetListsParams{BrandID: "12"})
+ if err != nil {
+ t.Errorf("Unexpected err %v", err)
+ }
+
+ if _, ok := outMap["list1"]; !ok {
+ t.Errorf("list1 is expected in the parsed map")
+ }
+
+ if outMap["list1"].ID != "xyz" || outMap["list1"].Name != "Netlandish Inc." {
+ t.Errorf("A List struct with ID: \"xyz\" and Name: \"Netlandish Inc.\" is expected")
+ }
+
+}
+
+func TestGetListsError(t *testing.T) {
+ GetPostFormFunc = func(url string, data url.Values) (*http.Response, error) {
+ return createMockResponse("No lists found"), nil
+ }
+ client := sendy.NewClient("", "", false)
+ client.SetClient(MockClient{})
+ _, err := client.GetLists(sendy.GetListsParams{BrandID: "12"})
+
+ if !errors.Is(err, sendy.ErrAPIResponse) {
+ t.Errorf("Expected %v, got %v", sendy.ErrAPIResponse, err)
+ }
+}
A => go.mod +3 -0
@@ 0,0 1,3 @@
+module hg.code.netlandish.com/~netlandish/sendygo
+
+go 1.18
A => params.go +191 -0
@@ 0,0 1,191 @@
+package sendy
+
+import (
+ "errors"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+// ErrParamsMissing is raised when the required params are missing
+var ErrParamsMissing = errors.New("required params missing")
+
+// CleanParams strips out empty parameters
+func CleanParams(values url.Values) {
+ for k, v := range values {
+ // if the Values only have one element and that
+ // element is an empty string
+ if strings.TrimSpace(v[0]) == "" && len(v) == 1 {
+ values.Del(k)
+ }
+ }
+}
+
+// ValidateParams validates required params
+func ValidateParams(values url.Values, required []string) error {
+ for _, v := range required {
+ if !values.Has(v) {
+ return ErrParamsMissing
+ }
+ }
+ return nil
+
+}
+
+// SusbcribeParams ...
+//
+// Email: user's email
+// List: the list id you want to subscribe a user to
+// Name: user's name (optional)
+// Country: user's 2 letter country code (optional)
+// IPAddress: user's IP address (optional)
+// Referrer: the URL where the user signed up from (optional)
+// GDPR: if you're signing up EU users in a GDPR compliant manner, set this to "true" (optional)
+// Silent: set to true if your list is 'Double opt-in' but you want to bypass that
+// and signup the user to the list as 'Single Opt-in instead' (optional)
+// HP: include this 'honeypot' field to prevent spambots from signing up via this API call. (optional)
+type SusbcribeParams struct {
+ List string
+ Email string
+ Name string
+ Country string
+ IPAddress string
+ Referrer string
+ Silent bool
+ GDPR bool
+ HP string
+}
+
+func (s SusbcribeParams) buildParams() url.Values {
+ data := url.Values{}
+ data.Set("list", s.List)
+ data.Set("email", s.Email)
+ data.Set("name", s.Name)
+ data.Set("country", s.Country)
+ data.Set("ipaddress", s.IPAddress)
+ data.Set("silent", strconv.FormatBool(s.Silent))
+ data.Set("gdpr", strconv.FormatBool(s.GDPR))
+ data.Set("hp", s.HP)
+ CleanParams(data)
+ return data
+}
+
+// UnsusbcribeParams ...
+//
+// Email: user's email
+// List: the list id you want to subscribe a user to
+type UnsusbcribeParams struct {
+ List string
+ Email string
+}
+
+func (s UnsusbcribeParams) buildParams() url.Values {
+ data := url.Values{}
+ data.Set("list", s.List)
+ data.Set("email", s.Email)
+ CleanParams(data)
+ return data
+}
+
+// ActionParams is aimed to perform actions over already
+// registered email like subscription_status, delete and subscriber_count
+//
+// Email: user's email
+// List: the list id you want to subscribe a user to
+type ActionParams struct {
+ List string
+ Email string
+}
+
+func (s ActionParams) buildParams() url.Values {
+ data := url.Values{}
+ data.Set("list_id", s.List)
+ data.Set("email", s.Email)
+ CleanParams(data)
+ return data
+}
+
+// CreateCampaignParams ...
+//
+// FormName: the 'From name' of your campaign
+// FromEmail: the 'From email' of your campaign
+// ReplyTo: the 'Reply to' of your campaign
+// Title: the 'Title' of your campaign
+// Subject: the 'Subject' of your campaign
+// PlainText: the 'Plain text version' of your campaign (optional)
+// HTMLText: the 'HTML version' of your campaign
+// ListIDS: Required only if you set SendCampaign to 1 and no SegmentIDS are passed in.
+// List IDs should be single or comma-separated.
+// SegmentIDS: Required only if you set send_campaign to 1 and no list_ids are passed in.
+// Segment IDs should be single or comma-separated.
+// ExcludeList: ids Lists to exclude from your campaign. List IDs should be single or comma-separated.(optional)
+// ExcludeSegmentsIDS: Segments to exclude from your campaign. Segment IDs should be single or comma-separated. (optional)
+// BrandID: Required only if you are creating a 'Draft' campaign
+// QueryString: eg. Google Analytics tags (optional)
+// TrackOpens: Set to 0 to disable, 1 to enable and 2 for anonymous opens tracking.
+// TrackClicks: Set to 0 to disable, 1 to enable and 2 for anonymous clicks tracking.
+// SendCampaign: Set to 1 if you want to send the campaign as well and not just create a draft. Default is 0.
+// ScheduleDateTime: Campaign will be scheduled if a valid date/time is passed. Date/time format. eg. June 15, 2021 6:05pm.
+// ScheduleTimeZone: Eg. 'America/New_York'.
+type CreateCampaignParams struct {
+ FromName string
+ FromEmail string
+ ReplyTo string
+ Title string
+ Subject string
+ PlainText string
+ HTMLText string
+ ListIDS string
+ BrandID string
+ QueryString string
+ SendCampaign int64
+ SegmentIDS string
+ ExcludeListIDS string
+ ExcludeSegmentsIDS string
+ TrackOpens int64
+ TrackClicks int64
+ ScheduleDateTime string
+ ScheduleTimeZone string
+}
+
+func (s CreateCampaignParams) buildParams() url.Values {
+ data := url.Values{}
+ data.Set("from_name", s.FromName)
+ data.Set("from_email", s.FromEmail)
+ data.Set("reply_to", s.ReplyTo)
+ data.Set("title", s.Title)
+ data.Set("subject", s.Subject)
+ data.Set("plain_text", s.PlainText)
+ data.Set("html_text", s.HTMLText)
+ data.Set("list_ids", s.ListIDS)
+ data.Set("brand_id", s.BrandID)
+ data.Set("query_string", s.QueryString)
+ data.Set("send_campaign", strconv.Itoa(int(s.SendCampaign)))
+ data.Set("segment_ids", s.SegmentIDS)
+ data.Set("exclude_list_ids", s.ExcludeListIDS)
+ data.Set("exclude_segments_ids", s.ExcludeSegmentsIDS)
+ data.Set("track_opens", strconv.Itoa(int(s.TrackOpens)))
+ data.Set("track_clicks", strconv.Itoa(int(s.TrackClicks)))
+ data.Set("schedule_date_time", s.ScheduleDateTime)
+ data.Set("schedule_timezone", s.ScheduleTimeZone)
+ CleanParams(data)
+ return data
+}
+
+// GetListsParams ...
+//
+// BrandID: the id of the brand you want to get the list of lists from.
+// IncludeHidden: if you want to retrieve lists that are hidden as well, set this to yes. Default is no.
+type GetListsParams struct {
+ BrandID string
+ IncludeHidden string
+}
+
+func (s GetListsParams) buildParams() url.Values {
+ data := url.Values{}
+ data.Set("brand_id", s.BrandID)
+ if s.IncludeHidden != "" {
+ data.Set("include_hidden", s.IncludeHidden)
+ }
+ return data
+}
A => params_test.go +45 -0
@@ 0,0 1,45 @@
+package sendy_test
+
+import (
+ "errors"
+ "net/url"
+ "testing"
+
+ sendy "hg.code.netlandish.com/~netlandish/sendygo"
+)
+
+func TestCleanParams(t *testing.T) {
+ values := url.Values{}
+ values.Set("email", "yader@netlandish.com")
+ values.Set("list", "one, two")
+ values.Set("name", "")
+
+ expected := 2
+ sendy.CleanParams(values)
+
+ if len(values) != expected {
+ t.Errorf("Expected %d, got %d", expected, len(values))
+ }
+
+}
+
+func TestValidateParams(t *testing.T) {
+ values := url.Values{}
+ values.Set("email", "yader@netlandish.com")
+
+ err := sendy.ValidateParams(values, []string{"email"})
+ if err != nil {
+ t.Errorf("Expected no error, got %v", err)
+ }
+
+}
+
+func TestValidateParamsError(t *testing.T) {
+ values := url.Values{}
+ values.Set("email", "yader@netlandish.com")
+ err := sendy.ValidateParams(values, []string{"email", "list"})
+ if !errors.Is(err, sendy.ErrParamsMissing) {
+ t.Errorf("Expected ErrParamsMissing error, got %v", err)
+ }
+
+}