From a5fcbbf9016d405864a65d8d3f299074d3ef80ee Mon Sep 17 00:00:00 2001 From: Vishal Date: Tue, 2 Apr 2024 17:36:53 +0530 Subject: [PATCH 1/9] add listmonk mailing list control (subscribe/usubscribe) --- server/configurations/local.yaml | 8 ++ server/pkg/controller/mailing_lists.go | 111 +++++++++++++++++++++---- server/pkg/external/listmonk/api.go | 104 +++++++++++++++++++++++ 3 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 server/pkg/external/listmonk/api.go diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index 97dd353e1f..2cfdba18b5 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -237,6 +237,14 @@ zoho: list-key: topic-ids: +# Listmonk Campaigns config (optional) +# Use case: Sending emails +listmonk: + server-url: + username: + password: + list-ids: + # Various low-level configuration options internal: # If false (the default), then museum will notify the external world of diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index 0cd51e54f4..f0b60adb58 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -6,29 +6,30 @@ import ( "strings" "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/external/listmonk" "github.com/ente-io/museum/pkg/external/zoho" "github.com/ente-io/stacktrace" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) -// MailingListsController is used to keeping the external mailing lists in sync +// ZohoMailingListsController is used to keeping the external mailing lists in sync // with customer email changes. // -// MailingListsController contains methods for keeping external mailing lists in +// ZohoMailingListsController contains methods for keeping external mailing lists in // sync when new users sign up, or update their email, or delete their account. // Currently, these mailing lists are hosted on Zoho Campaigns. // // See also: Syncing emails with Zoho Campaigns -type MailingListsController struct { +type ZohoMailingListsController struct { zohoAccessToken string zohoListKey string zohoTopicIds string zohoCredentials zoho.Credentials } -// Return a new instance of MailingListsController -func NewMailingListsController() *MailingListsController { +// Return a new instance of ZohoMailingListsController +func NewZohoMailingListsController() *ZohoMailingListsController { zohoCredentials := zoho.Credentials{ ClientID: viper.GetString("zoho.client-id"), ClientSecret: viper.GetString("zoho.client-secret"), @@ -57,7 +58,7 @@ func NewMailingListsController() *MailingListsController { // we'll use the refresh token to create an access token on demand. zohoAccessToken := viper.GetString("zoho.access_token") - return &MailingListsController{ + return &ZohoMailingListsController{ zohoCredentials: zohoCredentials, zohoListKey: zohoListKey, zohoTopicIds: zohoTopicIds, @@ -65,6 +66,31 @@ func NewMailingListsController() *MailingListsController { } } +// ListmonkMailingListsController is used to interact with the Listmonk API. +// +// It specifies BaseURL (URL of your listmonk server), +// your listmonk Username and Password +// and ListIDs (an array of integer values indicating the id of listmonk campaign mailing list +// to which the subscriber needs to added) +type ListmonkMailingListsController struct { + BaseURL string + Username string + Password string + ListIDs []int +} + +// NewListmonkMailingListsController creates a new instance of ListmonkMailingListsController +// with the API credentials provided in config file +func NewListmonkMailingListsController() *ListmonkMailingListsController { + credentials := &ListmonkMailingListsController{ + BaseURL: viper.GetString("listmonk.server-url"), + Username: viper.GetString("listmonk.username"), + Password: viper.GetString("listmonk.password"), + ListIDs: viper.GetIntSlice("listmonk.list-ids"), + } + return credentials +} + // Add the given email address to our default Zoho Campaigns list. // // It is valid to resubscribe an email that has previously been unsubscribe. @@ -75,8 +101,8 @@ func NewMailingListsController() *MailingListsController { // that can be later updated or deleted via their API. So instead, we maintain // the email addresses of our customers in a Zoho Campaign "list", and subscribe // or unsubscribe them to this list. -func (c *MailingListsController) Subscribe(email string) error { - if c.shouldSkip() { +func (c *ZohoMailingListsController) Subscribe(email string) error { + if c.shouldSkipZoho() { return stacktrace.Propagate(ente.ErrNotImplemented, "") } @@ -89,24 +115,26 @@ func (c *MailingListsController) Subscribe(email string) error { // any confirmations. // // https://www.zoho.com/campaigns/help/developers/contact-subscribe.html - return c.doListAction("listsubscribe", email) + return c.doListActionZoho("listsubscribe", email) } // Unsubscribe the given email address to our default Zoho Campaigns list. // // See: [Note: Syncing emails with Zoho Campaigns] -func (c *MailingListsController) Unsubscribe(email string) error { - if c.shouldSkip() { +func (c *ZohoMailingListsController) Unsubscribe(email string) error { + if c.shouldSkipZoho() { return stacktrace.Propagate(ente.ErrNotImplemented, "") } // https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html - return c.doListAction("listunsubscribe", email) + return c.doListActionZoho("listunsubscribe", email) } -func (c *MailingListsController) shouldSkip() bool { +// shouldSkipZoho checks if the ZohoMailingListsController should be skipped +// due to missing credentials. +func (c *ZohoMailingListsController) shouldSkipZoho() bool { if c.zohoCredentials.RefreshToken == "" { - log.Info("Skipping mailing list update because credentials are not configured") + log.Info("Skipping Zoho mailing list update because credentials are not configured") return true } return false @@ -114,7 +142,7 @@ func (c *MailingListsController) shouldSkip() bool { // Both the listsubscribe and listunsubscribe Zoho Campaigns API endpoints work // similarly, so use this function to keep the common code. -func (c *MailingListsController) doListAction(action string, email string) error { +func (c *ZohoMailingListsController) doListActionZoho(action string, email string) error { // Query escape the email so that any pluses get converted to %2B. escapedEmail := url.QueryEscape(email) contactInfo := fmt.Sprintf("{Contact+Email: \"%s\"}", escapedEmail) @@ -158,3 +186,56 @@ func (c *MailingListsController) doListAction(action string, email string) error return stacktrace.Propagate(err, "") } + +// Add or subscribe an email to listmonk mailing list +func (c *ListmonkMailingListsController) Subscribe(email string) error { + if c.shouldSkipListmonk() { + return stacktrace.Propagate(ente.ErrNotImplemented, "") + } + + data := map[string]interface{}{ + "email": email, + "lists": c.ListIDs, + } + + return listmonk.SendRequest("POST", c.BaseURL+"/api/subscribers", data, + c.Username, c.Password) +} + +// Remove or unsubscribe an email from listmonk mailing list +func (c *ListmonkMailingListsController) Unsubscribe(email string) error { + if c.shouldSkipListmonk() { + return stacktrace.Propagate(ente.ErrNotImplemented, "") + } + + // Listmonk dosen't provide an endpoint for unsubscribing users from a particular list + // directly via their email + // + // Thus, fetching subscriberID through email address, + // and then calling endpoint to modify subscription in a list + id, err := listmonk.GetSubscriberID(c.BaseURL+"/api/subscribers", c.Username, c.Password, email) + if err != nil { + stacktrace.Propagate(err, "") + } + // API endpoint expects an array of subscriber id as paarmeter + subscriberID := []int{id} + + data := map[string]interface{}{ + "ids": subscriberID, + "action": "unsubscribe", + "target_list_ids": c.ListIDs, + } + + return listmonk.SendRequest("PUT", c.BaseURL+"/api/subscribers/lists", data, + c.Username, c.Password) +} + +// shouldSkipListmonk checks if the ListmonkMailingListsController should be skipped +// due to missing credentials. +func (c *ListmonkMailingListsController) shouldSkipListmonk() bool { + if c.BaseURL == "" || c.Username == "" || c.Password == "" { + log.Info("Skipping Listmonk mailing list because credentials are not configured") + return true + } + return false +} diff --git a/server/pkg/external/listmonk/api.go b/server/pkg/external/listmonk/api.go new file mode 100644 index 0000000000..13750cc676 --- /dev/null +++ b/server/pkg/external/listmonk/api.go @@ -0,0 +1,104 @@ +package listmonk + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/ente-io/stacktrace" +) + +// GetSubscriberID returns subscriber id of the provided email address, else returns an error if email was not found +func GetSubscriberID(endpoint string, username string, password string, subscriberEmail string) (int, error) { + // Struct for the received API response. + // Can define other fields as well that can be extracted from response JSON + type SubscriberResponse struct { + Data struct { + Results []struct { + ID int `json:"id"` + } `json:"results"` + } `json:"data"` + } + + // Constructing query parameters + queryParams := url.Values{} + queryParams.Set("query", fmt.Sprintf("subscribers.email = '%s'", subscriberEmail)) + + // Constructing the URL with query parameters + endpointURL, err := url.Parse(endpoint) + if err != nil { + return 0, stacktrace.Propagate(err, "") + } + endpointURL.RawQuery = queryParams.Encode() + + req, err := http.NewRequest("GET", endpointURL.String(), nil) + if err != nil { + return 0, stacktrace.Propagate(err, "") + } + + req.SetBasicAuth(username, password) + + // Sending the HTTP request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return 0, stacktrace.Propagate(err, "") + } + defer resp.Body.Close() + + // Reading the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, stacktrace.Propagate(err, "") + } + + // Parsing the JSON response + var subscriberResp SubscriberResponse + if err := json.Unmarshal(body, &subscriberResp); err != nil { + return 0, stacktrace.Propagate(err, "") + } + + // Checking if there are any subscribers found + if len(subscriberResp.Data.Results) == 0 { + return 0, stacktrace.Propagate(err, "") + } + + // Extracting the ID from the response + id := subscriberResp.Data.Results[0].ID + + return id, nil +} + +// SendRequest sends a request to the specified Listmonk API endpoint with the provided method and data +// after authentication with the provided credentials (username, password) +func SendRequest(method string, url string, data interface{}, username string, password string) error { + jsonData, err := json.Marshal(data) + if err != nil { + return stacktrace.Propagate(err, "") + } + + client := &http.Client{} + req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonData)) + if err != nil { + return stacktrace.Propagate(err, "") + } + + req.SetBasicAuth(username, password) + req.Header.Set("Content-Type", "application/json") + + // Send request + resp, err := client.Do(req) + if err != nil { + return stacktrace.Propagate(err, "") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return stacktrace.Propagate(err, "") + } + + return nil +} From b8100b1273dae4b4f6fa37de7dcca69f3d642392 Mon Sep 17 00:00:00 2001 From: Vishal Date: Tue, 2 Apr 2024 17:45:30 +0530 Subject: [PATCH 2/9] rename functions --- server/pkg/controller/mailing_lists.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index f0b60adb58..cef08629d7 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -13,23 +13,23 @@ import ( "github.com/spf13/viper" ) -// ZohoMailingListsController is used to keeping the external mailing lists in sync +// MailingListsController is used to keeping the external mailing lists in sync // with customer email changes. // -// ZohoMailingListsController contains methods for keeping external mailing lists in +// MailingListsController contains methods for keeping external mailing lists in // sync when new users sign up, or update their email, or delete their account. // Currently, these mailing lists are hosted on Zoho Campaigns. // // See also: Syncing emails with Zoho Campaigns -type ZohoMailingListsController struct { +type MailingListsController struct { zohoAccessToken string zohoListKey string zohoTopicIds string zohoCredentials zoho.Credentials } -// Return a new instance of ZohoMailingListsController -func NewZohoMailingListsController() *ZohoMailingListsController { +// Return a new instance of MailingListsController +func NewMailingListsController() *MailingListsController { zohoCredentials := zoho.Credentials{ ClientID: viper.GetString("zoho.client-id"), ClientSecret: viper.GetString("zoho.client-secret"), @@ -58,7 +58,7 @@ func NewZohoMailingListsController() *ZohoMailingListsController { // we'll use the refresh token to create an access token on demand. zohoAccessToken := viper.GetString("zoho.access_token") - return &ZohoMailingListsController{ + return &MailingListsController{ zohoCredentials: zohoCredentials, zohoListKey: zohoListKey, zohoTopicIds: zohoTopicIds, @@ -101,7 +101,7 @@ func NewListmonkMailingListsController() *ListmonkMailingListsController { // that can be later updated or deleted via their API. So instead, we maintain // the email addresses of our customers in a Zoho Campaign "list", and subscribe // or unsubscribe them to this list. -func (c *ZohoMailingListsController) Subscribe(email string) error { +func (c *MailingListsController) Subscribe(email string) error { if c.shouldSkipZoho() { return stacktrace.Propagate(ente.ErrNotImplemented, "") } @@ -121,7 +121,7 @@ func (c *ZohoMailingListsController) Subscribe(email string) error { // Unsubscribe the given email address to our default Zoho Campaigns list. // // See: [Note: Syncing emails with Zoho Campaigns] -func (c *ZohoMailingListsController) Unsubscribe(email string) error { +func (c *MailingListsController) Unsubscribe(email string) error { if c.shouldSkipZoho() { return stacktrace.Propagate(ente.ErrNotImplemented, "") } @@ -130,9 +130,9 @@ func (c *ZohoMailingListsController) Unsubscribe(email string) error { return c.doListActionZoho("listunsubscribe", email) } -// shouldSkipZoho checks if the ZohoMailingListsController should be skipped +// shouldSkipZoho checks if the MailingListsController should be skipped // due to missing credentials. -func (c *ZohoMailingListsController) shouldSkipZoho() bool { +func (c *MailingListsController) shouldSkipZoho() bool { if c.zohoCredentials.RefreshToken == "" { log.Info("Skipping Zoho mailing list update because credentials are not configured") return true @@ -142,7 +142,7 @@ func (c *ZohoMailingListsController) shouldSkipZoho() bool { // Both the listsubscribe and listunsubscribe Zoho Campaigns API endpoints work // similarly, so use this function to keep the common code. -func (c *ZohoMailingListsController) doListActionZoho(action string, email string) error { +func (c *MailingListsController) doListActionZoho(action string, email string) error { // Query escape the email so that any pluses get converted to %2B. escapedEmail := url.QueryEscape(email) contactInfo := fmt.Sprintf("{Contact+Email: \"%s\"}", escapedEmail) From 18c48c7e0a30cfb29f679bba0dbe92a9ce5c870e Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 3 Apr 2024 11:14:55 +0530 Subject: [PATCH 3/9] Fix typo in comment --- server/pkg/controller/mailing_lists.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index cef08629d7..23a9a24079 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -217,7 +217,7 @@ func (c *ListmonkMailingListsController) Unsubscribe(email string) error { if err != nil { stacktrace.Propagate(err, "") } - // API endpoint expects an array of subscriber id as paarmeter + // API endpoint expects an array of subscriber id as parameter subscriberID := []int{id} data := map[string]interface{}{ From 01f842c4455e61a98cc3fe62510812906c203740 Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 3 Apr 2024 12:41:18 +0530 Subject: [PATCH 4/9] Rearrange methods --- server/pkg/controller/mailing_lists.go | 145 ++++++++++++++----------- 1 file changed, 80 insertions(+), 65 deletions(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index 23a9a24079..0a0954b4f7 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -70,8 +70,8 @@ func NewMailingListsController() *MailingListsController { // // It specifies BaseURL (URL of your listmonk server), // your listmonk Username and Password -// and ListIDs (an array of integer values indicating the id of listmonk campaign mailing list -// to which the subscriber needs to added) +// and ListIDs (an array of integer values indicating the id of +// listmonk campaign mailing list to which the subscriber needs to added) type ListmonkMailingListsController struct { BaseURL string Username string @@ -79,8 +79,8 @@ type ListmonkMailingListsController struct { ListIDs []int } -// NewListmonkMailingListsController creates a new instance of ListmonkMailingListsController -// with the API credentials provided in config file +// NewListmonkMailingListsController creates a new instance +// of ListmonkMailingListsController with the config file credentials func NewListmonkMailingListsController() *ListmonkMailingListsController { credentials := &ListmonkMailingListsController{ BaseURL: viper.GetString("listmonk.server-url"), @@ -102,36 +102,58 @@ func NewListmonkMailingListsController() *ListmonkMailingListsController { // the email addresses of our customers in a Zoho Campaign "list", and subscribe // or unsubscribe them to this list. func (c *MailingListsController) Subscribe(email string) error { - if c.shouldSkipZoho() { - return stacktrace.Propagate(ente.ErrNotImplemented, "") - } + listmonkController := NewListmonkMailingListsController() + var err error - // Need to set "Signup Form Disabled" in the list settings since we use this - // list to keep track of emails that have already been verified. - // - // > You can use this API to add contacts to your mailing lists. For signup - // form enabled mailing lists, the contacts will receive a confirmation - // email. For signup form disabled lists, contacts will be added without - // any confirmations. - // - // https://www.zoho.com/campaigns/help/developers/contact-subscribe.html - return c.doListActionZoho("listsubscribe", email) + // Checking if either listmonk or zoho credentials are configured + if c.shouldSkipZoho() { + err = stacktrace.Propagate(ente.ErrNotImplemented, "") + + if listmonkController.shouldSkipListmonk() { + err = stacktrace.Propagate(ente.ErrNotImplemented, "") + } else { + err = listmonkController.doListActionListmonk("listsubscribe", email) + } + + } else { + // Need to set "Signup Form Disabled" in the list settings since we use this + // list to keep track of emails that have already been verified. + // + // > You can use this API to add contacts to your mailing lists. For signup + // form enabled mailing lists, the contacts will receive a confirmation + // email. For signup form disabled lists, contacts will be added without + // any confirmations. + // + // https://www.zoho.com/campaigns/help/developers/contact-subscribe.html + err = c.doListActionZoho("listsubscribe", email) + } + return err } // Unsubscribe the given email address to our default Zoho Campaigns list. // // See: [Note: Syncing emails with Zoho Campaigns] func (c *MailingListsController) Unsubscribe(email string) error { - if c.shouldSkipZoho() { - return stacktrace.Propagate(ente.ErrNotImplemented, "") - } + listmonkController := NewListmonkMailingListsController() + var err error - // https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html - return c.doListActionZoho("listunsubscribe", email) + if c.shouldSkipZoho() { + err = stacktrace.Propagate(ente.ErrNotImplemented, "") + + if listmonkController.shouldSkipListmonk() { + err = stacktrace.Propagate(ente.ErrNotImplemented, "") + } else { + err = listmonkController.doListActionListmonk("listunsubscribe", email) + } + } else { + // https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html + err = c.doListActionZoho("listunsubscribe", email) + } + return err } -// shouldSkipZoho checks if the MailingListsController should be skipped -// due to missing credentials. +// shouldSkipZoho checks if the MailingListsController +// should be skipped due to missing credentials. func (c *MailingListsController) shouldSkipZoho() bool { if c.zohoCredentials.RefreshToken == "" { log.Info("Skipping Zoho mailing list update because credentials are not configured") @@ -187,51 +209,44 @@ func (c *MailingListsController) doListActionZoho(action string, email string) e return stacktrace.Propagate(err, "") } -// Add or subscribe an email to listmonk mailing list -func (c *ListmonkMailingListsController) Subscribe(email string) error { - if c.shouldSkipListmonk() { - return stacktrace.Propagate(ente.ErrNotImplemented, "") - } +// doListActionListmonk subscribes or unsubscribes an email address +// to a particular mailing list based on the action parameter +func (c *ListmonkMailingListsController) doListActionListmonk(action string, email string) error { + if action == "listsubscribe" { + data := map[string]interface{}{ + "email": email, + "lists": c.ListIDs, + } - data := map[string]interface{}{ - "email": email, - "lists": c.ListIDs, - } + return listmonk.SendRequest("POST", c.BaseURL+"/api/subscribers", data, + c.Username, c.Password) - return listmonk.SendRequest("POST", c.BaseURL+"/api/subscribers", data, - c.Username, c.Password) + } else { + // Listmonk dosen't provide an endpoint for unsubscribing users + // from a particular list directly via their email + // + // Thus, fetching subscriberID through email address, + // and then calling endpoint to modify subscription in a list + id, err := listmonk.GetSubscriberID(c.BaseURL+"/api/subscribers", c.Username, c.Password, email) + if err != nil { + stacktrace.Propagate(err, "") + } + // API endpoint expects an array of subscriber id as parameter + subscriberID := []int{id} + + data := map[string]interface{}{ + "ids": subscriberID, + "action": "unsubscribe", + "target_list_ids": c.ListIDs, + } + + return listmonk.SendRequest("PUT", c.BaseURL+"/api/subscribers/lists", data, + c.Username, c.Password) + } } -// Remove or unsubscribe an email from listmonk mailing list -func (c *ListmonkMailingListsController) Unsubscribe(email string) error { - if c.shouldSkipListmonk() { - return stacktrace.Propagate(ente.ErrNotImplemented, "") - } - - // Listmonk dosen't provide an endpoint for unsubscribing users from a particular list - // directly via their email - // - // Thus, fetching subscriberID through email address, - // and then calling endpoint to modify subscription in a list - id, err := listmonk.GetSubscriberID(c.BaseURL+"/api/subscribers", c.Username, c.Password, email) - if err != nil { - stacktrace.Propagate(err, "") - } - // API endpoint expects an array of subscriber id as parameter - subscriberID := []int{id} - - data := map[string]interface{}{ - "ids": subscriberID, - "action": "unsubscribe", - "target_list_ids": c.ListIDs, - } - - return listmonk.SendRequest("PUT", c.BaseURL+"/api/subscribers/lists", data, - c.Username, c.Password) -} - -// shouldSkipListmonk checks if the ListmonkMailingListsController should be skipped -// due to missing credentials. +// shouldSkipListmonk checks if the ListmonkMailingListsController +// should be skipped due to missing credentials. func (c *ListmonkMailingListsController) shouldSkipListmonk() bool { if c.BaseURL == "" || c.Username == "" || c.Password == "" { log.Info("Skipping Listmonk mailing list because credentials are not configured") From 39ec7619493b3b2b623600bfba63ce964a36a21f Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 3 Apr 2024 12:57:37 +0530 Subject: [PATCH 5/9] fix warnings --- server/pkg/controller/mailing_lists.go | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index 0a0954b4f7..56d67bf758 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -103,18 +103,14 @@ func NewListmonkMailingListsController() *ListmonkMailingListsController { // or unsubscribe them to this list. func (c *MailingListsController) Subscribe(email string) error { listmonkController := NewListmonkMailingListsController() - var err error // Checking if either listmonk or zoho credentials are configured if c.shouldSkipZoho() { - err = stacktrace.Propagate(ente.ErrNotImplemented, "") - if listmonkController.shouldSkipListmonk() { - err = stacktrace.Propagate(ente.ErrNotImplemented, "") + return stacktrace.Propagate(ente.ErrNotImplemented, "") } else { - err = listmonkController.doListActionListmonk("listsubscribe", email) + return listmonkController.doListActionListmonk("listsubscribe", email) } - } else { // Need to set "Signup Form Disabled" in the list settings since we use this // list to keep track of emails that have already been verified. @@ -125,9 +121,8 @@ func (c *MailingListsController) Subscribe(email string) error { // any confirmations. // // https://www.zoho.com/campaigns/help/developers/contact-subscribe.html - err = c.doListActionZoho("listsubscribe", email) + return c.doListActionZoho("listsubscribe", email) } - return err } // Unsubscribe the given email address to our default Zoho Campaigns list. @@ -135,21 +130,17 @@ func (c *MailingListsController) Subscribe(email string) error { // See: [Note: Syncing emails with Zoho Campaigns] func (c *MailingListsController) Unsubscribe(email string) error { listmonkController := NewListmonkMailingListsController() - var err error if c.shouldSkipZoho() { - err = stacktrace.Propagate(ente.ErrNotImplemented, "") - if listmonkController.shouldSkipListmonk() { - err = stacktrace.Propagate(ente.ErrNotImplemented, "") + return stacktrace.Propagate(ente.ErrNotImplemented, "") } else { - err = listmonkController.doListActionListmonk("listunsubscribe", email) + return listmonkController.doListActionListmonk("listunsubscribe", email) } } else { // https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html - err = c.doListActionZoho("listunsubscribe", email) + return c.doListActionZoho("listunsubscribe", email) } - return err } // shouldSkipZoho checks if the MailingListsController From ffefae89a69163421feef7001288943d4e741551 Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 3 Apr 2024 17:50:53 +0530 Subject: [PATCH 6/9] Redefine struct --- server/pkg/controller/mailing_lists.go | 119 ++++++++++++------------- server/pkg/external/listmonk/api.go | 20 ++++- 2 files changed, 74 insertions(+), 65 deletions(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index 56d67bf758..ffedfde90d 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -22,10 +22,12 @@ import ( // // See also: Syncing emails with Zoho Campaigns type MailingListsController struct { - zohoAccessToken string - zohoListKey string - zohoTopicIds string - zohoCredentials zoho.Credentials + zohoAccessToken string + zohoListKey string + zohoTopicIds string + zohoCredentials zoho.Credentials + listmonkListIDs []int + listmonkCredentials listmonk.Credentials } // Return a new instance of MailingListsController @@ -58,40 +60,28 @@ func NewMailingListsController() *MailingListsController { // we'll use the refresh token to create an access token on demand. zohoAccessToken := viper.GetString("zoho.access_token") - return &MailingListsController{ - zohoCredentials: zohoCredentials, - zohoListKey: zohoListKey, - zohoTopicIds: zohoTopicIds, - zohoAccessToken: zohoAccessToken, - } -} - -// ListmonkMailingListsController is used to interact with the Listmonk API. -// -// It specifies BaseURL (URL of your listmonk server), -// your listmonk Username and Password -// and ListIDs (an array of integer values indicating the id of -// listmonk campaign mailing list to which the subscriber needs to added) -type ListmonkMailingListsController struct { - BaseURL string - Username string - Password string - ListIDs []int -} - -// NewListmonkMailingListsController creates a new instance -// of ListmonkMailingListsController with the config file credentials -func NewListmonkMailingListsController() *ListmonkMailingListsController { - credentials := &ListmonkMailingListsController{ + listmonkCredentials := listmonk.Credentials{ BaseURL: viper.GetString("listmonk.server-url"), Username: viper.GetString("listmonk.username"), Password: viper.GetString("listmonk.password"), - ListIDs: viper.GetIntSlice("listmonk.list-ids"), } - return credentials + + // An array of integer values indicating the id of listmonk campaign + // mailing list to which the subscriber needs to added + listmonkListIDs := viper.GetIntSlice("listmonk.list-ids") + + return &MailingListsController{ + zohoCredentials: zohoCredentials, + zohoListKey: zohoListKey, + zohoTopicIds: zohoTopicIds, + zohoAccessToken: zohoAccessToken, + listmonkCredentials: listmonkCredentials, + listmonkListIDs: listmonkListIDs, + } } -// Add the given email address to our default Zoho Campaigns list. +// Add the given email address to our default Zoho Campaigns list +// or Listmonk Campaigns List // // It is valid to resubscribe an email that has previously been unsubscribe. // @@ -102,14 +92,12 @@ func NewListmonkMailingListsController() *ListmonkMailingListsController { // the email addresses of our customers in a Zoho Campaign "list", and subscribe // or unsubscribe them to this list. func (c *MailingListsController) Subscribe(email string) error { - listmonkController := NewListmonkMailingListsController() - // Checking if either listmonk or zoho credentials are configured if c.shouldSkipZoho() { - if listmonkController.shouldSkipListmonk() { + if c.shouldSkipListmonk() { return stacktrace.Propagate(ente.ErrNotImplemented, "") } else { - return listmonkController.doListActionListmonk("listsubscribe", email) + return c.doListActionListmonk("listsubscribe", email) } } else { // Need to set "Signup Form Disabled" in the list settings since we use this @@ -125,17 +113,16 @@ func (c *MailingListsController) Subscribe(email string) error { } } -// Unsubscribe the given email address to our default Zoho Campaigns list. +// Unsubscribe the given email address to our default Zoho Campaigns list +// or Listmonk Campaigns List // // See: [Note: Syncing emails with Zoho Campaigns] func (c *MailingListsController) Unsubscribe(email string) error { - listmonkController := NewListmonkMailingListsController() - if c.shouldSkipZoho() { - if listmonkController.shouldSkipListmonk() { + if c.shouldSkipListmonk() { return stacktrace.Propagate(ente.ErrNotImplemented, "") } else { - return listmonkController.doListActionListmonk("listunsubscribe", email) + return c.doListActionListmonk("listunsubscribe", email) } } else { // https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html @@ -143,7 +130,7 @@ func (c *MailingListsController) Unsubscribe(email string) error { } } -// shouldSkipZoho checks if the MailingListsController +// shouldSkipZoho() checks if the MailingListsController // should be skipped due to missing credentials. func (c *MailingListsController) shouldSkipZoho() bool { if c.zohoCredentials.RefreshToken == "" { @@ -153,6 +140,22 @@ func (c *MailingListsController) shouldSkipZoho() bool { return false } +// shouldSkipListmonk() checks if the Listmonk mailing list +// should be skipped due to missing credentials +// listmonklistIDs value. +// +// ListmonkListIDs is an optional field for subscribing an email address +// (user gets added to the default list), +// but is a required field for unsubscribing an email address +func (c *MailingListsController) shouldSkipListmonk() bool { + if c.listmonkCredentials.BaseURL == "" || c.listmonkCredentials.Username == "" || + c.listmonkCredentials.Password == "" || len(c.listmonkListIDs) == 0 { + log.Info("Skipping Listmonk mailing list because credentials are not configured") + return true + } + return false +} + // Both the listsubscribe and listunsubscribe Zoho Campaigns API endpoints work // similarly, so use this function to keep the common code. func (c *MailingListsController) doListActionZoho(action string, email string) error { @@ -200,17 +203,18 @@ func (c *MailingListsController) doListActionZoho(action string, email string) e return stacktrace.Propagate(err, "") } -// doListActionListmonk subscribes or unsubscribes an email address -// to a particular mailing list based on the action parameter -func (c *ListmonkMailingListsController) doListActionListmonk(action string, email string) error { +// doListActionListmonk() subscribes or unsubscribes an email address +// to a particular mailing list +// based on the action parameter ("listsubscribe" or "listunsubscribe") +func (c *MailingListsController) doListActionListmonk(action string, email string) error { if action == "listsubscribe" { data := map[string]interface{}{ "email": email, - "lists": c.ListIDs, + "lists": c.listmonkListIDs, } - return listmonk.SendRequest("POST", c.BaseURL+"/api/subscribers", data, - c.Username, c.Password) + return listmonk.SendRequest("POST", c.listmonkCredentials.BaseURL+"/api/subscribers", data, + c.listmonkCredentials.Username, c.listmonkCredentials.Password) } else { // Listmonk dosen't provide an endpoint for unsubscribing users @@ -218,7 +222,8 @@ func (c *ListmonkMailingListsController) doListActionListmonk(action string, ema // // Thus, fetching subscriberID through email address, // and then calling endpoint to modify subscription in a list - id, err := listmonk.GetSubscriberID(c.BaseURL+"/api/subscribers", c.Username, c.Password, email) + id, err := listmonk.GetSubscriberID(c.listmonkCredentials.BaseURL+"/api/subscribers", + c.listmonkCredentials.Username, c.listmonkCredentials.Password, email) if err != nil { stacktrace.Propagate(err, "") } @@ -228,20 +233,10 @@ func (c *ListmonkMailingListsController) doListActionListmonk(action string, ema data := map[string]interface{}{ "ids": subscriberID, "action": "unsubscribe", - "target_list_ids": c.ListIDs, + "target_list_ids": c.listmonkListIDs, } - return listmonk.SendRequest("PUT", c.BaseURL+"/api/subscribers/lists", data, - c.Username, c.Password) + return listmonk.SendRequest("PUT", c.listmonkCredentials.BaseURL+"/api/subscribers/lists", data, + c.listmonkCredentials.Username, c.listmonkCredentials.Password) } } - -// shouldSkipListmonk checks if the ListmonkMailingListsController -// should be skipped due to missing credentials. -func (c *ListmonkMailingListsController) shouldSkipListmonk() bool { - if c.BaseURL == "" || c.Username == "" || c.Password == "" { - log.Info("Skipping Listmonk mailing list because credentials are not configured") - return true - } - return false -} diff --git a/server/pkg/external/listmonk/api.go b/server/pkg/external/listmonk/api.go index 13750cc676..338a54c3ec 100644 --- a/server/pkg/external/listmonk/api.go +++ b/server/pkg/external/listmonk/api.go @@ -11,10 +11,23 @@ import ( "github.com/ente-io/stacktrace" ) -// GetSubscriberID returns subscriber id of the provided email address, else returns an error if email was not found +// Listmonk credentials to interact with the Listmonk API. +// It specifies BaseURL (url of the running listmonk server, +// Listmonk Username and Password. +// Visit https://listmonk.app/ to learn more about running +// Listmonk locally +type Credentials struct { + BaseURL string + Username string + Password string +} + +// GetSubscriberID returns subscriber id of the provided email address, +// else returns an error if email was not found func GetSubscriberID(endpoint string, username string, password string, subscriberEmail string) (int, error) { // Struct for the received API response. - // Can define other fields as well that can be extracted from response JSON + // Can define other fields as well that can be + // extracted from response JSON type SubscriberResponse struct { Data struct { Results []struct { @@ -72,7 +85,8 @@ func GetSubscriberID(endpoint string, username string, password string, subscrib return id, nil } -// SendRequest sends a request to the specified Listmonk API endpoint with the provided method and data +// SendRequest sends a request to the specified Listmonk API endpoint +// with the provided method and data // after authentication with the provided credentials (username, password) func SendRequest(method string, url string, data interface{}, username string, password string) error { jsonData, err := json.Marshal(data) From 2ddf4c897cb88ef275de24a2c7465ad72ddb0952 Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 3 Apr 2024 18:25:41 +0530 Subject: [PATCH 7/9] Rectify if else --- server/pkg/controller/mailing_lists.go | 92 ++++++++++++-------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index ffedfde90d..c44d9c27f9 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -92,14 +92,7 @@ func NewMailingListsController() *MailingListsController { // the email addresses of our customers in a Zoho Campaign "list", and subscribe // or unsubscribe them to this list. func (c *MailingListsController) Subscribe(email string) error { - // Checking if either listmonk or zoho credentials are configured - if c.shouldSkipZoho() { - if c.shouldSkipListmonk() { - return stacktrace.Propagate(ente.ErrNotImplemented, "") - } else { - return c.doListActionListmonk("listsubscribe", email) - } - } else { + if !(c.shouldSkipZoho()) { // Need to set "Signup Form Disabled" in the list settings since we use this // list to keep track of emails that have already been verified. // @@ -111,6 +104,10 @@ func (c *MailingListsController) Subscribe(email string) error { // https://www.zoho.com/campaigns/help/developers/contact-subscribe.html return c.doListActionZoho("listsubscribe", email) } + if !(c.shouldSkipListmonk()) { + return c.listmonkSubscribe(email) + } + return stacktrace.Propagate(ente.ErrNotImplemented, "") } // Unsubscribe the given email address to our default Zoho Campaigns list @@ -118,16 +115,14 @@ func (c *MailingListsController) Subscribe(email string) error { // // See: [Note: Syncing emails with Zoho Campaigns] func (c *MailingListsController) Unsubscribe(email string) error { - if c.shouldSkipZoho() { - if c.shouldSkipListmonk() { - return stacktrace.Propagate(ente.ErrNotImplemented, "") - } else { - return c.doListActionListmonk("listunsubscribe", email) - } - } else { + if !(c.shouldSkipZoho()) { // https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html return c.doListActionZoho("listunsubscribe", email) } + if !(c.shouldSkipListmonk()) { + return c.listmonkUnsubscribe(email) + } + return stacktrace.Propagate(ente.ErrNotImplemented, "") } // shouldSkipZoho() checks if the MailingListsController @@ -203,40 +198,37 @@ func (c *MailingListsController) doListActionZoho(action string, email string) e return stacktrace.Propagate(err, "") } -// doListActionListmonk() subscribes or unsubscribes an email address -// to a particular mailing list -// based on the action parameter ("listsubscribe" or "listunsubscribe") -func (c *MailingListsController) doListActionListmonk(action string, email string) error { - if action == "listsubscribe" { - data := map[string]interface{}{ - "email": email, - "lists": c.listmonkListIDs, - } - - return listmonk.SendRequest("POST", c.listmonkCredentials.BaseURL+"/api/subscribers", data, - c.listmonkCredentials.Username, c.listmonkCredentials.Password) - - } else { - // Listmonk dosen't provide an endpoint for unsubscribing users - // from a particular list directly via their email - // - // Thus, fetching subscriberID through email address, - // and then calling endpoint to modify subscription in a list - id, err := listmonk.GetSubscriberID(c.listmonkCredentials.BaseURL+"/api/subscribers", - c.listmonkCredentials.Username, c.listmonkCredentials.Password, email) - if err != nil { - stacktrace.Propagate(err, "") - } - // API endpoint expects an array of subscriber id as parameter - subscriberID := []int{id} - - data := map[string]interface{}{ - "ids": subscriberID, - "action": "unsubscribe", - "target_list_ids": c.listmonkListIDs, - } - - return listmonk.SendRequest("PUT", c.listmonkCredentials.BaseURL+"/api/subscribers/lists", data, - c.listmonkCredentials.Username, c.listmonkCredentials.Password) +// Subscribes an email address to a particular listmonk campaign mailing list +func (c *MailingListsController) listmonkSubscribe(email string) error { + data := map[string]interface{}{ + "email": email, + "lists": c.listmonkListIDs, } + return listmonk.SendRequest("POST", c.listmonkCredentials.BaseURL+"/api/subscribers", data, + c.listmonkCredentials.Username, c.listmonkCredentials.Password) +} + +// Unsubscribes an email address to a particular listmonk campaign mailing list +func (c *MailingListsController) listmonkUnsubscribe(email string) error { + // Listmonk dosen't provide an endpoint for unsubscribing users + // from a particular list directly via their email + // + // Thus, fetching subscriberID through email address, + // and then calling endpoint to modify subscription in a list + id, err := listmonk.GetSubscriberID(c.listmonkCredentials.BaseURL+"/api/subscribers", + c.listmonkCredentials.Username, c.listmonkCredentials.Password, email) + if err != nil { + stacktrace.Propagate(err, "") + } + // API endpoint expects an array of subscriber id as parameter + subscriberID := []int{id} + + data := map[string]interface{}{ + "ids": subscriberID, + "action": "unsubscribe", + "target_list_ids": c.listmonkListIDs, + } + + return listmonk.SendRequest("PUT", c.listmonkCredentials.BaseURL+"/api/subscribers/lists", data, + c.listmonkCredentials.Username, c.listmonkCredentials.Password) } From 92715b658c905eb2153de264c2673ba7ea0531ef Mon Sep 17 00:00:00 2001 From: Vishal Date: Wed, 3 Apr 2024 19:24:12 +0530 Subject: [PATCH 8/9] Change API parameter --- server/pkg/controller/mailing_lists.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index c44d9c27f9..98f64a0acc 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -225,7 +225,7 @@ func (c *MailingListsController) listmonkUnsubscribe(email string) error { data := map[string]interface{}{ "ids": subscriberID, - "action": "unsubscribe", + "action": "remove", "target_list_ids": c.listmonkListIDs, } From d8190926fd9a7360f513f766aa2d9ab4740329c8 Mon Sep 17 00:00:00 2001 From: Vishal Date: Thu, 4 Apr 2024 11:24:13 +0530 Subject: [PATCH 9/9] Change if-else --- server/pkg/controller/mailing_lists.go | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/server/pkg/controller/mailing_lists.go b/server/pkg/controller/mailing_lists.go index 98f64a0acc..c7eb0f1f34 100644 --- a/server/pkg/controller/mailing_lists.go +++ b/server/pkg/controller/mailing_lists.go @@ -5,7 +5,6 @@ import ( "net/url" "strings" - "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/external/listmonk" "github.com/ente-io/museum/pkg/external/zoho" "github.com/ente-io/stacktrace" @@ -102,12 +101,18 @@ func (c *MailingListsController) Subscribe(email string) error { // any confirmations. // // https://www.zoho.com/campaigns/help/developers/contact-subscribe.html - return c.doListActionZoho("listsubscribe", email) + err := c.doListActionZoho("listsubscribe", email) + if err != nil { + return stacktrace.Propagate(err, "") + } } if !(c.shouldSkipListmonk()) { - return c.listmonkSubscribe(email) + err := c.listmonkSubscribe(email) + if err != nil { + return stacktrace.Propagate(err, "") + } } - return stacktrace.Propagate(ente.ErrNotImplemented, "") + return nil } // Unsubscribe the given email address to our default Zoho Campaigns list @@ -117,12 +122,18 @@ func (c *MailingListsController) Subscribe(email string) error { func (c *MailingListsController) Unsubscribe(email string) error { if !(c.shouldSkipZoho()) { // https://www.zoho.com/campaigns/help/developers/contact-unsubscribe.html - return c.doListActionZoho("listunsubscribe", email) + err := c.doListActionZoho("listunsubscribe", email) + if err != nil { + return stacktrace.Propagate(err, "") + } } if !(c.shouldSkipListmonk()) { - return c.listmonkUnsubscribe(email) + err := c.listmonkUnsubscribe(email) + if err != nil { + return stacktrace.Propagate(err, "") + } } - return stacktrace.Propagate(ente.ErrNotImplemented, "") + return nil } // shouldSkipZoho() checks if the MailingListsController