From 49b763de5036c2e3d0c2207874a98dd6c305bc0f Mon Sep 17 00:00:00 2001 From: peter <1674920+peterbitfly@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:35:34 +0000 Subject: [PATCH] feat(notification): bye email if user reached daily email notification threshold --- backend/cmd/notification_sender/main.go | 17 +++++++++++++++ backend/pkg/commons/db/subscriptions.go | 29 +++++++++++-------------- backend/pkg/commons/mail/mail.go | 26 ++++++++++++++-------- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/backend/cmd/notification_sender/main.go b/backend/cmd/notification_sender/main.go index e78508597..a4840a89e 100644 --- a/backend/cmd/notification_sender/main.go +++ b/backend/cmd/notification_sender/main.go @@ -1,6 +1,7 @@ package notification_sender import ( + "context" "flag" "fmt" "net/http" @@ -8,6 +9,7 @@ import ( "sync" "time" + "github.com/go-redis/redis/v8" "github.com/gobitfly/beaconchain/pkg/commons/cache" "github.com/gobitfly/beaconchain/pkg/commons/db" "github.com/gobitfly/beaconchain/pkg/commons/log" @@ -147,6 +149,21 @@ func Run() { }() } + // Initialize the persistent redis client + wg.Add(1) + go func() { + defer wg.Done() + rdc := redis.NewClient(&redis.Options{ + Addr: cfg.RedisSessionStoreEndpoint, + ReadTimeout: time.Second * 60, + }) + + if err := rdc.Ping(context.Background()).Err(); err != nil { + log.Fatal(err, "error connecting to persistent redis store", 0) + } + db.PersistentRedisDbClient = rdc + }() + wg.Wait() defer db.ReaderDb.Close() diff --git a/backend/pkg/commons/db/subscriptions.go b/backend/pkg/commons/db/subscriptions.go index 639f70c8b..c81b533cb 100644 --- a/backend/pkg/commons/db/subscriptions.go +++ b/backend/pkg/commons/db/subscriptions.go @@ -1,7 +1,7 @@ package db import ( - "database/sql" + "context" "encoding/hex" "fmt" @@ -278,22 +278,19 @@ func UpdateSubscriptionLastSent(tx *sqlx.Tx, ts uint64, epoch uint64, subID uint return err } -// CountSentMail increases the count of sent mails in the table `mails_sent` for this day. -func CountSentMail(email string) error { +// CountSentMail increases the count of sent mails for this day. +func CountSentMail(email string) (int64, error) { day := time.Now().Truncate(utils.Day).Unix() - _, err := FrontendWriterDB.Exec(` - INSERT INTO mails_sent (email, ts, cnt) VALUES ($1, TO_TIMESTAMP($2), 1) - ON CONFLICT (email, ts) DO UPDATE SET cnt = mails_sent.cnt+1`, email, day) - return err -} + key := fmt.Sprintf("n_mails:%s:%d", email, day) -// GetMailsSentCount returns the number of sent mails for the day of the passed time. -func GetMailsSentCount(email string, t time.Time) (int, error) { - day := t.Truncate(utils.Day).Unix() - count := 0 - err := FrontendWriterDB.Get(&count, "SELECT cnt FROM mails_sent WHERE email = $1 AND ts = TO_TIMESTAMP($2)", email, day) - if err == sql.ErrNoRows { - return 0, nil + pipe := PersistentRedisDbClient.TxPipeline() + incr := pipe.Incr(context.Background(), key) + pipe.Expire(context.Background(), key, utils.Day) + _, err := pipe.Exec(context.Background()) + + if incr.Err() != nil { + return 0, incr.Err() } - return count, err + + return incr.Val(), err } diff --git a/backend/pkg/commons/mail/mail.go b/backend/pkg/commons/mail/mail.go index df0ae9e3f..ec56ba046 100644 --- a/backend/pkg/commons/mail/mail.go +++ b/backend/pkg/commons/mail/mail.go @@ -3,6 +3,7 @@ package mail import ( "bytes" "context" + "html/template" "fmt" "net/smtp" @@ -74,23 +75,30 @@ func createTextMessage(msg types.Email) string { func SendMailRateLimited(to, subject string, msg types.Email, attachment []types.EmailAttachment) error { if utils.Config.Frontend.MaxMailsPerEmailPerDay > 0 { now := time.Now() - count, err := db.GetMailsSentCount(to, now) + count, err := db.CountSentMail(to) if err != nil { return err } - if count >= utils.Config.Frontend.MaxMailsPerEmailPerDay { + if count > int64(utils.Config.Frontend.MaxMailsPerEmailPerDay) { timeLeft := now.Add(utils.Day).Truncate(utils.Day).Sub(now) return &types.RateLimitError{TimeLeft: timeLeft} + } else if count == int64(utils.Config.Frontend.MaxMailsPerEmailPerDay) { + // send an email if this was the last email for today + err := SendHTMLMail(to, + "beaconcha.in - Email notification threshold limit reached", + types.Email{ + Title: "Email notification threshold limit reached", + //nolint: gosec + Body: template.HTML(fmt.Sprintf("You have reached the email notification threshold limit of %d emails per day. Further notification emails will be suppressed until tomorrow.", utils.Config.Frontend.MaxMailsPerEmailPerDay)), + }, + []types.EmailAttachment{}) + if err != nil { + return err + } } } - err := db.CountSentMail(to) - if err != nil { - // only log if counting did not work - return fmt.Errorf("error counting sent email: %v", err) - } - - err = SendHTMLMail(to, subject, msg, attachment) + err := SendHTMLMail(to, subject, msg, attachment) if err != nil { return err }