b16d2b6c5f
Drives listmonk's real Campaign API from git-authored Markdown+frontmatter, so broadcast/segment emails get listmonk's mature unsubscribe/bulk-send/ attachment handling instead of reimplementing it. sync only ever creates or updates a draft (idempotent, diff-based, refuses to touch a non-draft campaign); a pushed send/<slug> tag or manual workflow run is the only way to actually trigger a send. Includes list-name resolution, segment_query materialization into managed lists, content-hash-deduped attachment uploads, and an automatic post-sync preview email.
111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
// Command campaigns drives listmonk's Campaign API from git-authored
|
|
// Markdown: sync creates/updates draft campaigns from campaigns/<slug>/
|
|
// directories, send transitions one to running (the one real send trigger
|
|
// in this tool), and test previews a campaign's current content without
|
|
// touching its status.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"reground.org/eec-campaigns/internal/campaign"
|
|
"reground.org/eec-campaigns/internal/listmonk"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
}
|
|
|
|
lm := listmonk.New(
|
|
mustEnv("LISTMONK_BASE_URL"),
|
|
mustEnv("LISTMONK_API_USER"),
|
|
mustEnv("LISTMONK_API_TOKEN"),
|
|
)
|
|
|
|
switch os.Args[1] {
|
|
case "sync":
|
|
if len(os.Args) < 3 {
|
|
fmt.Fprintln(os.Stderr, "usage: campaigns sync PATH")
|
|
os.Exit(1)
|
|
}
|
|
runSync(lm, os.Args[2])
|
|
case "send":
|
|
if len(os.Args) < 3 {
|
|
fmt.Fprintln(os.Stderr, "usage: campaigns send SLUG")
|
|
os.Exit(1)
|
|
}
|
|
runSend(lm, os.Args[2])
|
|
case "test":
|
|
if len(os.Args) < 4 {
|
|
fmt.Fprintln(os.Stderr, "usage: campaigns test SLUG EMAIL...")
|
|
os.Exit(1)
|
|
}
|
|
runTest(lm, os.Args[2], os.Args[3:])
|
|
default:
|
|
usage()
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintln(os.Stderr, "usage: campaigns <sync PATH|send SLUG|test SLUG EMAIL...>")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// runSync's default preview address(es) come from CAMPAIGNS_PREVIEW_EMAIL
|
|
// (comma-separated), used for any campaign that doesn't set its own
|
|
// preview_emails in frontmatter. Optional — sync still works with none set,
|
|
// it just won't auto-preview campaigns that don't specify their own.
|
|
func runSync(lm *listmonk.Client, path string) {
|
|
var defaultPreview []string
|
|
if raw := os.Getenv("CAMPAIGNS_PREVIEW_EMAIL"); raw != "" {
|
|
for _, addr := range strings.Split(raw, ",") {
|
|
if addr = strings.TrimSpace(addr); addr != "" {
|
|
defaultPreview = append(defaultPreview, addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
result, err := campaign.SyncDir(lm, path, defaultPreview)
|
|
if err != nil {
|
|
log.Fatalf("sync: %v", err)
|
|
}
|
|
log.Printf("synced: %v", result.Synced)
|
|
log.Printf("unchanged: %v", result.Unchanged)
|
|
if len(result.Rejected) > 0 {
|
|
log.Printf("rejected: %v", result.Rejected)
|
|
}
|
|
if len(result.PreviewFailed) > 0 {
|
|
log.Printf("preview send failed: %v", result.PreviewFailed)
|
|
}
|
|
if len(result.Rejected) > 0 || len(result.PreviewFailed) > 0 {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runSend(lm *listmonk.Client, slug string) {
|
|
camp, err := campaign.Send(lm, slug)
|
|
if err != nil {
|
|
log.Fatalf("send: %v", err)
|
|
}
|
|
log.Printf("sent: campaign %q (id=%d) is now running", slug, camp.ID)
|
|
}
|
|
|
|
func runTest(lm *listmonk.Client, slug string, emails []string) {
|
|
if err := campaign.Test(lm, slug, emails); err != nil {
|
|
log.Fatalf("test: %v", err)
|
|
}
|
|
log.Printf("test: sent preview of %q to %v", slug, emails)
|
|
}
|
|
|
|
func mustEnv(key string) string {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
log.Fatalf("%s environment variable is required", key)
|
|
}
|
|
return v
|
|
}
|