mirror of
https://github.com/knadh/listmonk.git
synced 2025-09-11 00:44:42 +08:00
Turn notifs
into a special stateful global singleton package, removing clunky deps.
This commit is contained in:
parent
e327ebbbdf
commit
e2f24a140e
11 changed files with 80 additions and 90 deletions
|
@ -14,6 +14,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/knadh/listmonk/internal/auth"
|
||||
"github.com/knadh/listmonk/internal/notifs"
|
||||
"github.com/knadh/listmonk/models"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lib/pq"
|
||||
|
@ -639,7 +640,7 @@ func (a *App) makeOptinCampaignMessage(o campReq) (campReq, error) {
|
|||
// Prepare sample opt-in message for the campaign.
|
||||
var b bytes.Buffer
|
||||
|
||||
if err := a.notifs.Tpls.ExecuteTemplate(&b, "optin-campaign", struct {
|
||||
if err := notifs.Tpls.ExecuteTemplate(&b, "optin-campaign", struct {
|
||||
Lists []models.List
|
||||
OptinURLAttr template.HTMLAttr
|
||||
}{lists, optinURLAttr}); err != nil {
|
||||
|
|
18
cmd/init.go
18
cmd/init.go
|
@ -484,11 +484,7 @@ func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
|
|||
}
|
||||
|
||||
// initCampaignManager initializes the campaign manager.
|
||||
func initCampaignManager(q *models.Queries, cs *constants, fnNotif notifs.FuncNotifSystem, co *core.Core, md media.Store, i *i18n.I18n) *manager.Manager {
|
||||
campNotifCB := func(subject string, data any) error {
|
||||
return fnNotif(subject, notifTplCampaign, data, nil)
|
||||
}
|
||||
|
||||
func initCampaignManager(q *models.Queries, cs *constants, co *core.Core, md media.Store, i *i18n.I18n) *manager.Manager {
|
||||
if ko.Bool("passive") {
|
||||
lo.Println("running in passive mode. won't process campaigns.")
|
||||
}
|
||||
|
@ -513,7 +509,7 @@ func initCampaignManager(q *models.Queries, cs *constants, fnNotif notifs.FuncNo
|
|||
SlidingWindowRate: ko.Int("app.message_sliding_window_rate"),
|
||||
ScanInterval: time.Second * 5,
|
||||
ScanCampaigns: !ko.Bool("passive"),
|
||||
}, newManagerStore(q, co, md), campNotifCB, i, lo)
|
||||
}, newManagerStore(q, co, md), i, lo)
|
||||
}
|
||||
|
||||
// initTxTemplates initializes and compiles the transactional templates and caches them in-memory.
|
||||
|
@ -545,12 +541,12 @@ func initImporter(q *models.Queries, db *sqlx.DB, core *core.Core, app *App) *su
|
|||
|
||||
// Hook for triggering admin notifications and refreshing stats materialized
|
||||
// views after a successful import.
|
||||
NotifCB: func(subject string, data any) error {
|
||||
PostCB: func(subject string, data any) error {
|
||||
// Refresh cached subscriber counts and stats.
|
||||
core.RefreshMatViews(true)
|
||||
|
||||
// Send admin notification.
|
||||
app.notifs.NotifySystem(subject, notifTplImport, data, nil)
|
||||
notifs.NotifySystem(subject, notifs.TplImport, data, nil)
|
||||
return nil
|
||||
},
|
||||
}, db.DB, app.i18n)
|
||||
|
@ -677,7 +673,7 @@ func initMediaStore(ko *koanf.Koanf) media.Store {
|
|||
}
|
||||
|
||||
// initNotifs initializes the notifier with the system e-mail templates.
|
||||
func initNotifs(fs stuffbin.FileSystem, i *i18n.I18n, pushFn notifs.FuncPush, cs *constants, ko *koanf.Koanf) *notifs.Notifs {
|
||||
func initNotifs(fs stuffbin.FileSystem, i *i18n.I18n, em *email.Emailer, cs *constants, ko *koanf.Koanf) {
|
||||
tpls, err := stuffbin.ParseTemplatesGlob(initTplFuncs(i, cs), fs, "/static/email-templates/*.html")
|
||||
if err != nil {
|
||||
lo.Fatalf("error parsing e-mail notif templates: %v", err)
|
||||
|
@ -701,11 +697,11 @@ func initNotifs(fs stuffbin.FileSystem, i *i18n.I18n, pushFn notifs.FuncPush, cs
|
|||
lo.Println("system e-mail templates are plaintext")
|
||||
}
|
||||
|
||||
return notifs.NewNotifs(notifs.Opt{
|
||||
notifs.Initialize(notifs.Opt{
|
||||
FromEmail: ko.MustString("app.from_email"),
|
||||
SystemEmails: ko.MustStrings("app.notify_emails"),
|
||||
ContentType: contentType,
|
||||
}, tpls, pushFn, lo)
|
||||
}, tpls, em, lo)
|
||||
}
|
||||
|
||||
// initBounceManager initializes the bounce manager that scans mailboxes and listens to webhooks
|
||||
|
|
13
cmd/main.go
13
cmd/main.go
|
@ -24,7 +24,7 @@ import (
|
|||
"github.com/knadh/listmonk/internal/i18n"
|
||||
"github.com/knadh/listmonk/internal/manager"
|
||||
"github.com/knadh/listmonk/internal/media"
|
||||
notifs "github.com/knadh/listmonk/internal/notifs"
|
||||
"github.com/knadh/listmonk/internal/messenger/email"
|
||||
"github.com/knadh/listmonk/internal/subimporter"
|
||||
"github.com/knadh/listmonk/models"
|
||||
"github.com/knadh/paginator"
|
||||
|
@ -53,7 +53,6 @@ type App struct {
|
|||
paginator *paginator.Paginator
|
||||
captcha *captcha.Captcha
|
||||
events *events.Events
|
||||
notifs *notifs.Notifs
|
||||
about about
|
||||
log *log.Logger
|
||||
bufLog *buflog.BufLog
|
||||
|
@ -216,7 +215,7 @@ func main() {
|
|||
})
|
||||
|
||||
app.queries = queries
|
||||
app.manager = initCampaignManager(app.queries, app.constants, app.notifs.NotifySystem, app.core, app.media, app.i18n)
|
||||
app.manager = initCampaignManager(app.queries, app.constants, app.core, app.media, app.i18n)
|
||||
app.importer = initImporter(app.queries, db, app.core, app)
|
||||
|
||||
hasUsers, auth := initAuth(app.core, db.DB, ko)
|
||||
|
@ -226,10 +225,6 @@ func main() {
|
|||
// for new user setup.
|
||||
app.needsUserSetup = !hasUsers
|
||||
|
||||
// Initialize admin email notification templates.
|
||||
app.notifs = initNotifs(app.fs, app.i18n, app.manager.PushMessage, app.constants, ko)
|
||||
initTxTemplates(app.manager, app.core)
|
||||
|
||||
// Initialize the bounce manager that processes bounces from webhooks and
|
||||
// POP3 mailbox scanning.
|
||||
if ko.Bool("bounce.enabled") {
|
||||
|
@ -245,6 +240,10 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize admin email notification templates.
|
||||
initNotifs(app.fs, app.i18n, app.emailMessenger.(*email.Emailer), app.constants, ko)
|
||||
initTxTemplates(app.manager, app.core)
|
||||
|
||||
// Initialize any additional postback messengers.
|
||||
app.messengers = append(app.messengers, initPostbackMessengers(ko)...)
|
||||
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
notifTplImport = "import-status"
|
||||
notifTplCampaign = "campaign-status"
|
||||
notifSubscriberOptin = "subscriber-optin"
|
||||
notifSubscriberData = "subscriber-data"
|
||||
)
|
||||
|
||||
var (
|
||||
reTitle = regexp.MustCompile(`(?s)<title\s*data-i18n\s*>(.+?)</title>`)
|
||||
)
|
||||
|
||||
// getTplSubject extrcts any custom i18n subject rendered in the given rendered
|
||||
// template body. If it's not found, the incoming subject and body are returned.
|
||||
func getTplSubject(subject string, body []byte) (string, []byte) {
|
||||
m := reTitle.FindSubmatch(body)
|
||||
if len(m) != 2 {
|
||||
return subject, body
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(m[1])), reTitle.ReplaceAll(body, []byte(""))
|
||||
}
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/knadh/listmonk/internal/i18n"
|
||||
"github.com/knadh/listmonk/internal/manager"
|
||||
"github.com/knadh/listmonk/internal/notifs"
|
||||
"github.com/knadh/listmonk/models"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/lib/pq"
|
||||
|
@ -563,13 +564,14 @@ func (a *App) SelfExportSubscriberData(c echo.Context) error {
|
|||
|
||||
// Prepare the attachment e-mail.
|
||||
var msg bytes.Buffer
|
||||
if err := a.notifs.Tpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
|
||||
a.log.Printf("error compiling notification template '%s': %v", notifSubscriberData, err)
|
||||
if err := notifs.Tpls.ExecuteTemplate(&msg, notifs.TplSubscriberData, data); err != nil {
|
||||
a.log.Printf("error compiling notification template '%s': %v", notifs.TplSubscriberData, err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
subject, body := getTplSubject(a.i18n.Ts("email.data.title"), msg.Bytes())
|
||||
// TODO: GetTplSubject should be moved to a utils package.
|
||||
subject, body := notifs.GetTplSubject(a.i18n.Ts("email.data.title"), msg.Bytes())
|
||||
|
||||
// E-mail the data as a JSON attachment to the subscriber.
|
||||
const fname = "data.json"
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/knadh/koanf/providers/rawbytes"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/knadh/listmonk/internal/messenger/email"
|
||||
"github.com/knadh/listmonk/internal/notifs"
|
||||
"github.com/knadh/listmonk/models"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
@ -323,7 +324,7 @@ func (a *App) TestSMTPSettings(c echo.Context) error {
|
|||
|
||||
// Render the test email template body.
|
||||
var b bytes.Buffer
|
||||
if err := a.notifs.Tpls.ExecuteTemplate(&b, "smtp-test", nil); err != nil {
|
||||
if err := notifs.Tpls.ExecuteTemplate(&b, "smtp-test", nil); err != nil {
|
||||
a.log.Printf("error compiling notification template '%s': %v", "smtp-test", err)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/knadh/listmonk/internal/auth"
|
||||
"github.com/knadh/listmonk/internal/notifs"
|
||||
"github.com/knadh/listmonk/internal/subimporter"
|
||||
"github.com/knadh/listmonk/models"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -595,7 +596,7 @@ func (app *App) optinConfirmNotify() func(sub models.Subscriber, listIDs []int)
|
|||
}
|
||||
|
||||
// Send the e-mail.
|
||||
if err := app.notifs.Notify([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out, hdr); err != nil {
|
||||
if err := notifs.Notify([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifs.TplSubscriberOptin, out, hdr); err != nil {
|
||||
app.log.Printf("error sending opt-in e-mail for subscriber %d (%s): %s", sub.ID, sub.UUID, err)
|
||||
return 0, err
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/knadh/listmonk/internal/i18n"
|
||||
"github.com/knadh/listmonk/internal/notifs"
|
||||
"github.com/knadh/listmonk/models"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
@ -64,7 +65,7 @@ type Manager struct {
|
|||
store Store
|
||||
i18n *i18n.I18n
|
||||
messengers map[string]Messenger
|
||||
notifCB models.NotifCallback
|
||||
fnNotify func(subject string, data any) error
|
||||
log *log.Logger
|
||||
|
||||
// Campaigns that are currently running.
|
||||
|
@ -145,7 +146,7 @@ type Config struct {
|
|||
var pushTimeout = time.Second * 3
|
||||
|
||||
// New returns a new instance of Mailer.
|
||||
func New(cfg Config, store Store, notifCB models.NotifCallback, i *i18n.I18n, l *log.Logger) *Manager {
|
||||
func New(cfg Config, store Store, i *i18n.I18n, l *log.Logger) *Manager {
|
||||
if cfg.BatchSize < 1 {
|
||||
cfg.BatchSize = 1000
|
||||
}
|
||||
|
@ -157,10 +158,12 @@ func New(cfg Config, store Store, notifCB models.NotifCallback, i *i18n.I18n, l
|
|||
}
|
||||
|
||||
m := &Manager{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
i18n: i,
|
||||
notifCB: notifCB,
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
i18n: i,
|
||||
fnNotify: func(subject string, data any) error {
|
||||
return notifs.NotifySystem(subject, notifs.TplCampaignStatus, data, nil)
|
||||
},
|
||||
log: l,
|
||||
messengers: make(map[string]Messenger),
|
||||
pipes: make(map[int]*pipe),
|
||||
|
@ -588,7 +591,7 @@ func (m *Manager) sendNotif(c *models.Campaign, status, reason string) error {
|
|||
}
|
||||
)
|
||||
|
||||
return m.notifCB(subject, data)
|
||||
return m.fnNotify(subject, data)
|
||||
}
|
||||
|
||||
// makeGnericFuncMap returns a generic template func map with custom template
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
// package notifs is a special singleton, stateful globally accessible package
|
||||
// that handles sending out arbitrary notifications to the admin and users.
|
||||
// It's initialized once in the main package and is accessed globally across
|
||||
// other packages.
|
||||
package notifs
|
||||
|
||||
import (
|
||||
|
@ -8,9 +12,17 @@ import (
|
|||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/listmonk/internal/messenger/email"
|
||||
"github.com/knadh/listmonk/models"
|
||||
)
|
||||
|
||||
const (
|
||||
TplImport = "import-status"
|
||||
TplCampaignStatus = "campaign-status"
|
||||
TplSubscriberOptin = "subscriber-optin"
|
||||
TplSubscriberData = "subscriber-data"
|
||||
)
|
||||
|
||||
type FuncPush func(msg models.Message) error
|
||||
type FuncNotif func(toEmails []string, subject, tplName string, data any, headers textproto.MIMEHeader) error
|
||||
type FuncNotifSystem func(subject, tplName string, data any, headers textproto.MIMEHeader) error
|
||||
|
@ -21,52 +33,58 @@ type Opt struct {
|
|||
ContentType string
|
||||
}
|
||||
|
||||
var (
|
||||
reTitle = regexp.MustCompile(`(?s)<title\s*data-i18n\s*>(.+?)</title>`)
|
||||
)
|
||||
|
||||
type Notifs struct {
|
||||
Tpls *template.Template
|
||||
pushFn FuncPush
|
||||
lo *log.Logger
|
||||
em *email.Emailer
|
||||
lo *log.Logger
|
||||
|
||||
opt Opt
|
||||
}
|
||||
|
||||
// NewNotifs returns a new Notifs instance.
|
||||
func NewNotifs(opt Opt, tpls *template.Template, pushFn FuncPush, lo *log.Logger) *Notifs {
|
||||
return &Notifs{
|
||||
opt: opt,
|
||||
Tpls: tpls,
|
||||
pushFn: pushFn,
|
||||
lo: lo,
|
||||
var (
|
||||
reTitle = regexp.MustCompile(`(?s)<title\s*data-i18n\s*>(.+?)</title>`)
|
||||
|
||||
Tpls *template.Template
|
||||
no *Notifs
|
||||
)
|
||||
|
||||
// Initialize returns a new Notifs instance.
|
||||
func Initialize(opt Opt, tpls *template.Template, em *email.Emailer, lo *log.Logger) {
|
||||
if no != nil {
|
||||
lo.Fatal("notifs already initialized")
|
||||
}
|
||||
|
||||
Tpls = tpls
|
||||
no = &Notifs{
|
||||
opt: opt,
|
||||
em: em,
|
||||
lo: lo,
|
||||
}
|
||||
}
|
||||
|
||||
// NotifySystem sends out an e-mail notification to the admin emails.
|
||||
func (n *Notifs) NotifySystem(subject, tplName string, data any, hdr textproto.MIMEHeader) error {
|
||||
return n.Notify(n.opt.SystemEmails, subject, tplName, data, hdr)
|
||||
func NotifySystem(subject, tplName string, data any, hdr textproto.MIMEHeader) error {
|
||||
return Notify(no.opt.SystemEmails, subject, tplName, data, hdr)
|
||||
}
|
||||
|
||||
// Notify sends out an e-mail notification.
|
||||
func (n *Notifs) Notify(toEmails []string, subject, tplName string, data any, hdr textproto.MIMEHeader) error {
|
||||
func Notify(toEmails []string, subject, tplName string, data any, hdr textproto.MIMEHeader) error {
|
||||
if len(toEmails) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := n.Tpls.ExecuteTemplate(&buf, tplName, data); err != nil {
|
||||
n.lo.Printf("error compiling notification template '%s': %v", tplName, err)
|
||||
if err := Tpls.ExecuteTemplate(&buf, tplName, data); err != nil {
|
||||
no.lo.Printf("error compiling notification template '%s': %v", tplName, err)
|
||||
return err
|
||||
}
|
||||
body := buf.Bytes()
|
||||
|
||||
subject, body = getTplSubject(subject, body)
|
||||
subject, body = GetTplSubject(subject, body)
|
||||
|
||||
m := models.Message{
|
||||
Messenger: "email",
|
||||
ContentType: n.opt.ContentType,
|
||||
From: n.opt.FromEmail,
|
||||
ContentType: no.opt.ContentType,
|
||||
From: no.opt.FromEmail,
|
||||
To: toEmails,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
|
@ -74,17 +92,17 @@ func (n *Notifs) Notify(toEmails []string, subject, tplName string, data any, hd
|
|||
}
|
||||
|
||||
// Send the message.
|
||||
if err := n.pushFn(m); err != nil {
|
||||
n.lo.Printf("error sending admin notification (%s): %v", subject, err)
|
||||
if err := no.em.Push(m); err != nil {
|
||||
no.lo.Printf("error sending admin notification (%s): %v", subject, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getTplSubject extracts any custom i18n subject rendered in the given rendered
|
||||
// GetTplSubject extracts any custom i18n subject rendered in the given rendered
|
||||
// template body. If it's not found, the incoming subject and body are returned.
|
||||
func getTplSubject(subject string, body []byte) (string, []byte) {
|
||||
func GetTplSubject(subject string, body []byte) (string, []byte) {
|
||||
m := reTitle.FindSubmatch(body)
|
||||
if len(m) != 2 {
|
||||
return subject, body
|
||||
|
|
|
@ -71,7 +71,7 @@ type Options struct {
|
|||
UpsertStmt *sql.Stmt
|
||||
BlocklistStmt *sql.Stmt
|
||||
UpdateListDateStmt *sql.Stmt
|
||||
NotifCB models.NotifCallback
|
||||
PostCB func(subject string, data any) error
|
||||
|
||||
DomainBlocklist []string
|
||||
DomainAllowlist []string
|
||||
|
@ -255,7 +255,7 @@ func (im *Importer) sendNotif(status string) error {
|
|||
}
|
||||
subject = fmt.Sprintf("%s: %s import", cases.Title(language.Und).String(status), s.Name)
|
||||
)
|
||||
return im.opt.NotifCB(subject, out)
|
||||
return im.opt.PostCB(subject, out)
|
||||
}
|
||||
|
||||
// Start is a blocking function that selects on a channel queue until all
|
||||
|
|
|
@ -119,9 +119,6 @@ var regTplFuncs = []regTplFunc{
|
|||
},
|
||||
}
|
||||
|
||||
// NotifCallback is a callback function that triggers a notification.
|
||||
type NotifCallback func(subject string, data any) error
|
||||
|
||||
// PageResults is a generic HTTP response container for paginated results of list of items.
|
||||
type PageResults struct {
|
||||
Results any `json:"results"`
|
||||
|
|
Loading…
Add table
Reference in a new issue