add listmonk mailing list control (subscribe/usubscribe)
This commit is contained in:
parent
6213628aee
commit
a5fcbbf901
3 changed files with 208 additions and 15 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
104
server/pkg/external/listmonk/api.go
vendored
Normal file
104
server/pkg/external/listmonk/api.go
vendored
Normal file
|
@ -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
|
||||
}
|
Loading…
Add table
Reference in a new issue