From adcd8cf82f7ec4027701643c545b1f27a7cfc221 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Fri, 24 Oct 2025 00:28:40 +0700 Subject: feat: send new blog posts to subscribers --- internal/blogtrigger/blogtrigger.go | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 internal/blogtrigger/blogtrigger.go (limited to 'internal/blogtrigger') diff --git a/internal/blogtrigger/blogtrigger.go b/internal/blogtrigger/blogtrigger.go new file mode 100644 index 0000000..ab66981 --- /dev/null +++ b/internal/blogtrigger/blogtrigger.go @@ -0,0 +1,68 @@ +package blogtrigger + +import ( + "fmt" + "log/slog" + + "github.com/SayaAndy/saya-today-web/config" + "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/go-co-op/gocron/v2" +) + +type BlogTriggerScheduler struct { + s gocron.Scheduler + knownBlogPages map[string]map[string]*b2.BlogPage + b2Client *b2.B2Client + onTrigger func([]*b2.BlogPage) error +} + +func NewBlogTriggerScheduler(b2Client *b2.B2Client, availableLanguages []config.AvailableLanguageConfig, onTrigger func([]*b2.BlogPage) error) (*BlogTriggerScheduler, error) { + s, err := gocron.NewScheduler() + if err != nil { + return nil, fmt.Errorf("failed to create new scheduler: %w", err) + } + + knownBlogPages := make(map[string]map[string]*b2.BlogPage, len(availableLanguages)) + for _, lang := range availableLanguages { + knownBlogPages[lang.Name] = make(map[string]*b2.BlogPage) + } + + bts := &BlogTriggerScheduler{s, knownBlogPages, b2Client, onTrigger} + defer bts.s.Start() + + bts.s.NewJob(gocron.CronJob("0/5 * * * *", false), gocron.NewTask(func(bts *BlogTriggerScheduler) { + posts, err := bts.scan() + if err != nil { + slog.Error("failed to execute scanning new blog pages cron job", slog.String("error", err.Error())) + return + } + if err = onTrigger(posts); err != nil { + slog.Error("error happened on callback function after scanning new blog pages", slog.String("error", err.Error())) + return + } + }, bts)) + + if _, err = bts.scan(); err != nil { + return nil, fmt.Errorf("failed to scan existing blog pages in b2: %w", err) + } + + return bts, nil +} + +func (bts *BlogTriggerScheduler) scan() (newPages []*b2.BlogPage, err error) { + newPages = make([]*b2.BlogPage, 0) + for lang := range bts.knownBlogPages { + posts, err := bts.b2Client.Scan(lang + "/") + if err != nil { + return nil, fmt.Errorf("failed to scan blog pages in b2 on '%s': %w", lang, err) + } + for _, post := range posts { + if _, ok := bts.knownBlogPages[lang][post.FileName]; !ok { + post.Lang = lang + newPages = append(newPages, post) + bts.knownBlogPages[lang][post.FileName] = post + } + } + } + return newPages, nil +} -- cgit v1.3.1+13 From 249f73a7eff43994ed91fb1e1bef8edf5ac6c86a Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Sat, 25 Oct 2025 18:57:48 +0700 Subject: feat: configurable cron for mail trigger --- config/config.go | 19 ++++++++++++------- config/config.local.yaml | 2 ++ config/config.prod.yaml | 2 ++ config/config.stage.yaml | 2 ++ internal/blogtrigger/blogtrigger.go | 4 ++-- main.go | 2 +- 6 files changed, 21 insertions(+), 10 deletions(-) (limited to 'internal/blogtrigger') diff --git a/config/config.go b/config/config.go index d5dab2d..1393dd8 100644 --- a/config/config.go +++ b/config/config.go @@ -62,13 +62,18 @@ type Sqlite3Config struct { } type MailConfig struct { - ClientHost string `json:"ClientHost" yaml:"clientHost" validate:"required"` - MailHost string `json:"MailHost" yaml:"mailHost" validate:"required"` - PublicName string `json:"PublicName" yaml:"publicName" validate:"required"` - MailAddress string `json:"MailAddress" yaml:"mailAddress" validate:"required"` - Username string `json:"Username" yaml:"username" validate:"required"` - Password string `json:"Password" yaml:"password" validate:"required"` - Salt string `json:"Salt" yaml:"salt" validate:"required"` + ClientHost string `json:"ClientHost" yaml:"clientHost" validate:"required"` + MailHost string `json:"MailHost" yaml:"mailHost" validate:"required"` + PublicName string `json:"PublicName" yaml:"publicName" validate:"required"` + MailAddress string `json:"MailAddress" yaml:"mailAddress" validate:"required"` + Username string `json:"Username" yaml:"username" validate:"required"` + Password string `json:"Password" yaml:"password" validate:"required"` + Salt string `json:"Salt" yaml:"salt" validate:"required"` + Trigger TriggerConfig `json:"Trigger" yaml:"trigger" validate:"required"` +} + +type TriggerConfig struct { + OnNewPost string `json:"OnNewPost" yaml:"onNewPost" validate:"cron,required"` } func LoadConfig(path string, config *Config) error { diff --git a/config/config.local.yaml b/config/config.local.yaml index 008fa93..d40e8e9 100644 --- a/config/config.local.yaml +++ b/config/config.local.yaml @@ -42,3 +42,5 @@ mail: username: '${MAIL_USERNAME}' password: '${MAIL_PASSWORD}' salt: '${MAIL_SALT}' + trigger: + onNewPost: "* * * * *" diff --git a/config/config.prod.yaml b/config/config.prod.yaml index 4c0f425..7a9e5ae 100644 --- a/config/config.prod.yaml +++ b/config/config.prod.yaml @@ -42,3 +42,5 @@ mail: username: '${MAIL_USERNAME}' password: '${MAIL_PASSWORD}' salt: '${MAIL_SALT}' + trigger: + onNewPost: "0/5 * * * *" diff --git a/config/config.stage.yaml b/config/config.stage.yaml index 003ec05..8a5fde1 100644 --- a/config/config.stage.yaml +++ b/config/config.stage.yaml @@ -42,3 +42,5 @@ mail: username: '${MAIL_USERNAME}' password: '${MAIL_PASSWORD}' salt: '${MAIL_SALT}' + trigger: + onNewPost: "0/3 * * * *" diff --git a/internal/blogtrigger/blogtrigger.go b/internal/blogtrigger/blogtrigger.go index ab66981..9b016e8 100644 --- a/internal/blogtrigger/blogtrigger.go +++ b/internal/blogtrigger/blogtrigger.go @@ -16,7 +16,7 @@ type BlogTriggerScheduler struct { onTrigger func([]*b2.BlogPage) error } -func NewBlogTriggerScheduler(b2Client *b2.B2Client, availableLanguages []config.AvailableLanguageConfig, onTrigger func([]*b2.BlogPage) error) (*BlogTriggerScheduler, error) { +func NewBlogTriggerScheduler(b2Client *b2.B2Client, availableLanguages []config.AvailableLanguageConfig, cron string, onTrigger func([]*b2.BlogPage) error) (*BlogTriggerScheduler, error) { s, err := gocron.NewScheduler() if err != nil { return nil, fmt.Errorf("failed to create new scheduler: %w", err) @@ -30,7 +30,7 @@ func NewBlogTriggerScheduler(b2Client *b2.B2Client, availableLanguages []config. bts := &BlogTriggerScheduler{s, knownBlogPages, b2Client, onTrigger} defer bts.s.Start() - bts.s.NewJob(gocron.CronJob("0/5 * * * *", false), gocron.NewTask(func(bts *BlogTriggerScheduler) { + bts.s.NewJob(gocron.CronJob(cron, false), gocron.NewTask(func(bts *BlogTriggerScheduler) { posts, err := bts.scan() if err != nil { slog.Error("failed to execute scanning new blog pages cron job", slog.String("error", err.Error())) diff --git a/main.go b/main.go index 9d80cee..bd552a1 100644 --- a/main.go +++ b/main.go @@ -153,7 +153,7 @@ func main() { os.Exit(1) } - router.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(b2Client, cfg.AvailableLanguages, func(bp []*b2.BlogPage) error { + router.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(b2Client, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost, func(bp []*b2.BlogPage) error { for _, post := range bp { if err := router.Mailer.NewPost(post); err != nil { return err -- cgit v1.3.1+13 From 634fa487aa91c1a12af0ddb0cb0744c27df12c04 Mon Sep 17 00:00:00 2001 From: SayaAndy Date: Sat, 25 Oct 2025 19:41:49 +0700 Subject: feat: enable WAL journal mode for db fix: green colors on unsubscribe page feat: properly shutdown blog trigger scheduler --- config/config.local.yaml | 2 +- config/config.prod.yaml | 2 +- config/config.stage.yaml | 2 +- internal/blogtrigger/blogtrigger.go | 4 ++++ internal/router/lang-user-unsubscribe.go | 2 +- main.go | 3 +++ 6 files changed, 11 insertions(+), 4 deletions(-) (limited to 'internal/blogtrigger') diff --git a/config/config.local.yaml b/config/config.local.yaml index d40e8e9..327bad4 100644 --- a/config/config.local.yaml +++ b/config/config.local.yaml @@ -32,7 +32,7 @@ auth: db: type: sqlite3 config: - dsn: 'file:/tmp/auth.db?cache=shared&mode=rwc' + dsn: 'file:/tmp/auth.db?cache=shared&mode=rwc&_journal_mode=WAL' salt: '123' mail: clientHost: '127.0.0.1:3000' diff --git a/config/config.prod.yaml b/config/config.prod.yaml index 7a9e5ae..0bee79d 100644 --- a/config/config.prod.yaml +++ b/config/config.prod.yaml @@ -32,7 +32,7 @@ auth: db: type: sqlite3 config: - dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2' + dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL' salt: '${AUTH_SALT}' mail: clientHost: '${FQDN}' diff --git a/config/config.stage.yaml b/config/config.stage.yaml index 8a5fde1..1c15dae 100644 --- a/config/config.stage.yaml +++ b/config/config.stage.yaml @@ -32,7 +32,7 @@ auth: db: type: sqlite3 config: - dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2' + dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL' salt: '${AUTH_SALT}' mail: clientHost: '${FQDN}' diff --git a/internal/blogtrigger/blogtrigger.go b/internal/blogtrigger/blogtrigger.go index 9b016e8..c5b3097 100644 --- a/internal/blogtrigger/blogtrigger.go +++ b/internal/blogtrigger/blogtrigger.go @@ -49,6 +49,10 @@ func NewBlogTriggerScheduler(b2Client *b2.B2Client, availableLanguages []config. return bts, nil } +func (bts *BlogTriggerScheduler) Close() error { + return bts.s.Shutdown() +} + func (bts *BlogTriggerScheduler) scan() (newPages []*b2.BlogPage, err error) { newPages = make([]*b2.BlogPage, 0) for lang := range bts.knownBlogPages { diff --git a/internal/router/lang-user-unsubscribe.go b/internal/router/lang-user-unsubscribe.go index a6ca89f..81d1bfb 100644 --- a/internal/router/lang-user-unsubscribe.go +++ b/internal/router/lang-user-unsubscribe.go @@ -45,7 +45,7 @@ func Lang_User_Unsubscribe(l map[string]*locale.LocaleConfig, langs []config.Ava statusText = l[lang].UnsubscribePage.OnServerError status = fiber.ErrInternalServerError.Code } else { - statusColor = "0, 255, 0," + statusColor = "0, 255, 0" statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡" statusText = l[lang].UnsubscribePage.Success status = fiber.StatusOK diff --git a/main.go b/main.go index bd552a1..174854d 100644 --- a/main.go +++ b/main.go @@ -202,6 +202,9 @@ func main() { <-sigChan slog.Info("gracefully shutting down...") + if err = router.BlogTrigger.Close(); err != nil { + slog.Error("fail to shutdown blog trigger scheduler", slog.String("error", err.Error())) + } if err = app.Shutdown(); err != nil { slog.Error("fail to shutdown fiber server", slog.String("error", err.Error())) } -- cgit v1.3.1+13