Refactor SendPulse into shared service, wire app registration to Sojorn Members list (book 568122)

This commit is contained in:
Patrick Britton 2026-02-06 15:33:58 -06:00
parent 90ff6e223b
commit 2ad148f607
3 changed files with 112 additions and 66 deletions

View file

@ -1,11 +1,7 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"os/signal"
@ -116,6 +112,7 @@ func main() {
notificationService := services.NewNotificationService(notifRepo, pushService, userRepo)
emailService := services.NewEmailService(cfg)
sendPulseService := services.NewSendPulseService(cfg.SendPulseID, cfg.SendPulseSecret)
// Load moderation configuration
moderationConfig := config.NewModerationConfig()
@ -133,7 +130,7 @@ func main() {
userHandler := handlers.NewUserHandler(userRepo, postRepo, notificationService, assetService)
postHandler := handlers.NewPostHandler(postRepo, userRepo, feedService, assetService, notificationService, moderationService, contentFilter)
chatHandler := handlers.NewChatHandler(chatRepo, notificationService, hub)
authHandler := handlers.NewAuthHandler(userRepo, cfg, emailService)
authHandler := handlers.NewAuthHandler(userRepo, cfg, emailService, sendPulseService)
categoryHandler := handlers.NewCategoryHandler(categoryRepo)
keyHandler := handlers.NewKeyHandler(userRepo)
backupHandler := handlers.NewBackupHandler(repository.NewBackupRepository(dbPool))
@ -200,10 +197,8 @@ func main() {
c.JSON(500, gin.H{"error": "Failed to join waitlist"})
return
}
// Add to SendPulse in background
go func(email string) {
addToSendPulse(cfg, email)
}(req.Email)
// Add to SendPulse waitlist in background
go sendPulseService.AddToWaitlist(req.Email)
c.JSON(200, gin.H{"message": "You're on the list!"})
})
}
@ -466,55 +461,3 @@ func main() {
log.Info().Msg("Server exiting")
}
func addToSendPulse(cfg *config.Config, email string) {
if cfg.SendPulseID == "" || cfg.SendPulseSecret == "" {
return
}
// 1. Get OAuth token
tokenBody, _ := json.Marshal(map[string]string{
"grant_type": "client_credentials",
"client_id": cfg.SendPulseID,
"client_secret": cfg.SendPulseSecret,
})
tokenResp, err := http.Post("https://api.sendpulse.com/oauth/access_token", "application/json", bytes.NewReader(tokenBody))
if err != nil {
log.Error().Err(err).Msg("SendPulse: failed to get token")
return
}
defer tokenResp.Body.Close()
var tokenData struct {
AccessToken string `json:"access_token"`
}
if err := json.NewDecoder(tokenResp.Body).Decode(&tokenData); err != nil || tokenData.AccessToken == "" {
log.Error().Err(err).Msg("SendPulse: failed to parse token")
return
}
// 2. Add subscriber to Sojorn Waitlist (address book 568090)
subBody, _ := json.Marshal(map[string]interface{}{
"emails": []map[string]string{
{"email": email},
},
})
req, _ := http.NewRequest("POST", "https://api.sendpulse.com/addressbooks/568090/emails", bytes.NewReader(subBody))
req.Header.Set("Authorization", "Bearer "+tokenData.AccessToken)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error().Err(err).Msg("SendPulse: failed to add subscriber")
return
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
log.Error().Str("status", fmt.Sprintf("%d", resp.StatusCode)).Str("body", string(body)).Msg("SendPulse: add subscriber failed")
return
}
log.Info().Str("email", email).Msg("SendPulse: subscriber added to waitlist")
}

View file

@ -23,13 +23,14 @@ import (
)
type AuthHandler struct {
repo *repository.UserRepository
config *config.Config
emailService *services.EmailService
repo *repository.UserRepository
config *config.Config
emailService *services.EmailService
sendPulseService *services.SendPulseService
}
func NewAuthHandler(repo *repository.UserRepository, cfg *config.Config, emailService *services.EmailService) *AuthHandler {
return &AuthHandler{repo: repo, config: cfg, emailService: emailService}
func NewAuthHandler(repo *repository.UserRepository, cfg *config.Config, emailService *services.EmailService, sendPulseService *services.SendPulseService) *AuthHandler {
return &AuthHandler{repo: repo, config: cfg, emailService: emailService, sendPulseService: sendPulseService}
}
type RegisterRequest struct {
@ -154,6 +155,12 @@ func (h *AuthHandler) Register(c *gin.Context) {
log.Printf("[Auth] Failed to send email to %s: %v", req.Email, err)
}
}()
// Add to SendPulse Members list if user opted into newsletter
if req.EmailNewsletter && h.sendPulseService != nil {
go h.sendPulseService.AddToMembers(req.Email)
}
c.JSON(http.StatusCreated, gin.H{
"message": "Registration successful. Please verify your email to activate your account.",
"state": "verification_pending",

View file

@ -0,0 +1,96 @@
package services
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/rs/zerolog/log"
)
const (
SendPulseWaitlistBookID = 568090
SendPulseMembersBookID = 568122
)
type SendPulseService struct {
ClientID string
ClientSecret string
}
func NewSendPulseService(clientID, clientSecret string) *SendPulseService {
return &SendPulseService{ClientID: clientID, ClientSecret: clientSecret}
}
func (s *SendPulseService) getToken() (string, error) {
tokenBody, _ := json.Marshal(map[string]string{
"grant_type": "client_credentials",
"client_id": s.ClientID,
"client_secret": s.ClientSecret,
})
resp, err := http.Post("https://api.sendpulse.com/oauth/access_token", "application/json", bytes.NewReader(tokenBody))
if err != nil {
return "", err
}
defer resp.Body.Close()
var data struct {
AccessToken string `json:"access_token"`
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", err
}
if data.AccessToken == "" {
return "", fmt.Errorf("empty access token")
}
return data.AccessToken, nil
}
func (s *SendPulseService) AddSubscriber(bookID int, email string) {
if s.ClientID == "" || s.ClientSecret == "" {
return
}
token, err := s.getToken()
if err != nil {
log.Error().Err(err).Msg("SendPulse: failed to get token")
return
}
subBody, _ := json.Marshal(map[string]interface{}{
"emails": []map[string]string{
{"email": email},
},
})
url := fmt.Sprintf("https://api.sendpulse.com/addressbooks/%d/emails", bookID)
req, _ := http.NewRequest("POST", url, bytes.NewReader(subBody))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error().Err(err).Int("book_id", bookID).Msg("SendPulse: failed to add subscriber")
return
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
log.Error().Int("status", resp.StatusCode).Str("body", string(body)).Int("book_id", bookID).Msg("SendPulse: add subscriber failed")
return
}
log.Info().Str("email", email).Int("book_id", bookID).Msg("SendPulse: subscriber added")
}
// AddToWaitlist adds an email to the Sojorn Waitlist
func (s *SendPulseService) AddToWaitlist(email string) {
s.AddSubscriber(SendPulseWaitlistBookID, email)
}
// AddToMembers adds an email to the Sojorn Members list
func (s *SendPulseService) AddToMembers(email string) {
s.AddSubscriber(SendPulseMembersBookID, email)
}