summaryrefslogtreecommitdiff
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
-rw-r--r--config/config.go19
-rw-r--r--config/config.local.yaml5
-rw-r--r--config/config.prod.yaml4
-rw-r--r--config/config.stage.yaml4
-rw-r--r--internal/blogtrigger/blogtrigger.go8
-rw-r--r--internal/mailer/mailer.go71
-rw-r--r--internal/router/client-cache.go18
-rw-r--r--internal/router/lang-user-unsubscribe.go4
-rw-r--r--main.go5
-rw-r--r--views/pages/blog-catalogue.html8
10 files changed, 112 insertions, 34 deletions
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..e23c808 100644
--- a/config/config.local.yaml
+++ b/config/config.local.yaml
@@ -32,7 +32,8 @@ 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'
+ dsn: 'file:/tmp/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2&_journal_mode=WAL'
salt: '123'
mail:
clientHost: '127.0.0.1:3000'
@@ -42,3 +43,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..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}'
@@ -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..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}'
@@ -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..c5b3097 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()))
@@ -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/mailer/mailer.go b/internal/mailer/mailer.go
index c146548..d6dbd17 100644
--- a/internal/mailer/mailer.go
+++ b/internal/mailer/mailer.go
@@ -152,12 +152,20 @@ func (m *Mailer) MailIsTaken(email string) (bool, error) {
return false, fmt.Errorf("failed to initialize transaction with db: %s", err)
}
+ slog.Debug("began db transaction", slog.String("method", "MailIsTaken"))
+ defer func(tx *sql.Tx) {
+ if err = tx.Commit(); err != nil {
+ tx.Rollback()
+ }
+ slog.Debug("ended db transaction", slog.String("method", "MailIsTaken"))
+ }(tx)
+
var rows *sql.Rows
if rows, err = tx.Query(`SELECT email FROM user_email_table WHERE email=? LIMIT 1;`, email); err != nil {
tx.Rollback()
+
return false, fmt.Errorf("failed to query user-email settings in db: %s", err)
}
- defer tx.Commit()
defer rows.Close()
isTaken := rows.Next()
@@ -170,12 +178,19 @@ func (m *Mailer) GetInfo(userIdHash []byte) (email string, lang string, err erro
return "", "", fmt.Errorf("failed to initialize transaction with db: %s", err)
}
+ slog.Debug("began db transaction", slog.String("method", "GetInfo"))
+ defer func(tx *sql.Tx) {
+ if err = tx.Commit(); err != nil {
+ tx.Rollback()
+ }
+ slog.Debug("ended db transaction", slog.String("method", "GetInfo"))
+ }(tx)
+
var rows *sql.Rows
if rows, err = tx.Query(`SELECT email, lang FROM user_email_table WHERE user_id=? LIMIT 1;`, userIdHash); err != nil {
tx.Rollback()
return "", "", fmt.Errorf("failed to query user-email settings in db: %s", err)
}
- defer tx.Commit()
defer rows.Close()
if !rows.Next() {
@@ -302,18 +317,22 @@ func (m *Mailer) Verify(verificationCodeEncoded string, lang string) error {
return fmt.Errorf("failed to initialize transaction with db: %s", err)
}
+ slog.Debug("began db transaction", slog.String("method", "Verify"))
if _, err = tx.Exec(`INSERT INTO user_email_table(user_id, email, lang) VALUES(?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET
email=excluded.email,
lang=excluded.lang;`, m.GetHash(string(userId)), address, lang); err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "Verify"))
return fmt.Errorf("failed to configure user-email settings in db: %s", err)
}
if err = tx.Commit(); err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "Verify"))
return fmt.Errorf("failed to commit transaction to db: %s", err)
}
+ slog.Debug("ended db transaction", slog.String("method", "Verify"))
m.verificationCodes.Del(verificationCode)
delete(m.lostMailMap, verificationSegments[0])
@@ -326,6 +345,14 @@ func (m *Mailer) GetSubscriptions(userId string) (subscriptionType SubscriptionT
return None, nil, fmt.Errorf("failed to initialize transaction with db: %s", err)
}
+ slog.Debug("began db transaction", slog.String("method", "GetSubscriptions"))
+ defer func(tx *sql.Tx) {
+ if err = tx.Commit(); err != nil {
+ tx.Rollback()
+ }
+ slog.Debug("ended db transaction", slog.String("method", "GetSubscriptions"))
+ }(tx)
+
hash := m.GetHash(userId)
var rows *sql.Rows
@@ -333,7 +360,6 @@ func (m *Mailer) GetSubscriptions(userId string) (subscriptionType SubscriptionT
tx.Rollback()
return None, nil, fmt.Errorf("failed to query user-to-tags table in db for the user: %s", err)
}
- defer tx.Commit()
defer rows.Close()
if !rows.Next() {
@@ -361,6 +387,8 @@ func (m *Mailer) Subscribe(userIdHash []byte, subscriptionType SubscriptionType,
return fmt.Errorf("failed to initialize transaction with db: %s", err)
}
+ slog.Debug("began db transaction", slog.String("method", "Subscribe"))
+
slices.Sort(tags)
tagsOutput := ""
switch subscriptionType {
@@ -376,13 +404,16 @@ func (m *Mailer) Subscribe(userIdHash []byte, subscriptionType SubscriptionType,
ON CONFLICT(user_id) DO UPDATE SET
tags=excluded.tags;`, userIdHash, tagsOutput); err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
return fmt.Errorf("failed to configure user-to-tags table in db for the user: %s", err)
}
if err = tx.Commit(); err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
return fmt.Errorf("failed to commit transaction to db: %s", err)
}
+ slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
return nil
}
@@ -393,9 +424,11 @@ func (m *Mailer) NewPost(post *b2.BlogPage) error {
return fmt.Errorf("failed to initialize transaction with db: %s", err)
}
+ slog.Debug("began db transaction", slog.String("method", "NewPost"))
var rows *sql.Rows
if rows, err = tx.Query(`SELECT user_id, tags FROM subscription_user_to_tags_table;`); err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "NewPost"))
return fmt.Errorf("failed to query user-to-tags table in db: %s", err)
}
@@ -419,21 +452,11 @@ rowLoop:
continue
}
- email, lang, err := m.GetInfo(userId)
- if err != nil {
- slog.Warn("failed to get info about the user", slog.String("error", err.Error()), slog.Int("index", i), slog.String("user_id", base64.RawStdEncoding.EncodeToString(userId)))
- continue
- }
-
- if lang != post.Lang {
- continue
- }
-
if tagsString == "_all" {
usersToSend = append(usersToSend, struct {
userId []byte
email string
- }{userId, email})
+ }{userId, ""})
continue
}
@@ -443,7 +466,7 @@ rowLoop:
usersToSend = append(usersToSend, struct {
userId []byte
email string
- }{userId, email})
+ }{userId, ""})
continue rowLoop
}
}
@@ -451,9 +474,27 @@ rowLoop:
tx.Commit()
rows.Close()
+ slog.Debug("ended db transaction", slog.String("method", "NewPost"))
+
+ for i := range usersToSend {
+ email, lang, err := m.GetInfo(usersToSend[i].userId)
+ if err != nil {
+ slog.Warn("failed to get info about the user", slog.String("error", err.Error()), slog.Int("index", i), slog.String("user_id", base64.RawStdEncoding.EncodeToString(userId)))
+ continue
+ }
+
+ if lang != post.Lang {
+ continue
+ }
+
+ usersToSend[i].email = email
+ }
messages := make([]*mail.Msg, 0, len(usersToSend))
for _, user := range usersToSend {
+ if user.email == "" {
+ continue
+ }
unsubscribeCodeBytes := make([]byte, 8)
rand.Read(unsubscribeCodeBytes)
unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes)
diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go
index d607cdd..e7cda30 100644
--- a/internal/router/client-cache.go
+++ b/internal/router/client-cache.go
@@ -31,10 +31,12 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
if err != nil {
return nil, fmt.Errorf("fail to init transaction with db to fill cache: %w", err)
}
+ slog.Debug("began db transaction", slog.String("method", "NewClientCache"))
rows, err := tx.Query("select * from blog_likes;")
if err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return nil, fmt.Errorf("fail to query db for blog_likes to fill cache: %w", err)
}
@@ -62,6 +64,7 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
rows, err = tx.Query("select * from blog_views;")
if err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return nil, fmt.Errorf("fail to query db for blog_views to fill cache: %w", err)
}
@@ -81,8 +84,11 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) {
}
if err = tx.Commit(); err != nil {
+ tx.Rollback()
+ slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return nil, fmt.Errorf("fail to commit transaction in db: %w", err)
}
+ slog.Debug("ended db transaction", slog.String("method", "NewClientCache"))
return &ClientCache{
hashMap: make(map[string]string),
@@ -99,18 +105,28 @@ func (c *ClientCache) Close() error {
if err != nil {
return fmt.Errorf("fail to init transaction with db to dump cache: %w", err)
}
+ slog.Debug("began db transaction in ClientCache.Close")
if err = batchSave(tx, "blog_likes", c.likePageMap); err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction in ClientCache.Close")
return fmt.Errorf("fail to save blog_likes: %s", err)
}
if err = batchSave(tx, "blog_views", c.viewPageMap); err != nil {
tx.Rollback()
+ slog.Debug("ended db transaction in ClientCache.Close")
return fmt.Errorf("fail to save blog_views: %s", err)
}
- return tx.Commit()
+ if err = tx.Commit(); err != nil {
+ tx.Rollback()
+ slog.Debug("ended db transaction in ClientCache.Close")
+ return fmt.Errorf("fail to commit all the changes related to cache: %s", err)
+ }
+
+ slog.Debug("ended db transaction in ClientCache.Close")
+ return nil
}
func (c *ClientCache) GetHash(id string) string {
diff --git a/internal/router/lang-user-unsubscribe.go b/internal/router/lang-user-unsubscribe.go
index a6ca89f..c39f5e7 100644
--- a/internal/router/lang-user-unsubscribe.go
+++ b/internal/router/lang-user-unsubscribe.go
@@ -35,17 +35,19 @@ func Lang_User_Unsubscribe(l map[string]*locale.LocaleConfig, langs []config.Ava
statusText = l[lang].UnsubscribePage.UnsetCode
status = fiber.ErrBadRequest.Code
} else if clientError, serverError := Mailer.Unsubscribe(unsubscribeCode); clientError != nil {
+ slog.Info("got a client error when unsubscribing", slog.String("error", clientError.Error()))
statusColor = "255, 0, 0"
statusEmoji = "(͠≖~≖ ͡ )"
statusText = l[lang].UnsubscribePage.InvalidCode
status = fiber.ErrBadRequest.Code
} else if serverError != nil {
+ slog.Error("got a server error when unsubscribing", slog.String("error", serverError.Error()))
statusColor = "255, 128, 0"
statusEmoji = "( ˶°ㅁ°) !!"
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 9d80cee..174854d 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
@@ -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()))
}
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index 8e91798..d209205 100644
--- a/views/pages/blog-catalogue.html
+++ b/views/pages/blog-catalogue.html
@@ -1,10 +1,10 @@
{{ define "body" }}
-<div class="flex flex-row ar-lt-0.8:flex-col text-base h-[100%]">
+<div class="flex flex-row ar-lt-0.8:flex-col text-base h-full">
<form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML"
- class="tags-list ar-gt-0.8:min-w-[10dvh] ar-gt-0.8:max-w-[20dvh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] ar-lt-0.8:max-h-[30%] flex shrink-0 flex-col ar-gt-0.8:mr-6 bg-background-light inset-shadow-[0_0_0.4rem_black]">
+ class="tags-list ar-gt-0.8:min-w-[10dvh] ar-gt-0.8:max-w-[20dvh] ar-gt-0.8:w-fit ar-lt-0.8:w-full ar-lt-0.8:max-h-[30%] flex shrink-0 flex-col ar-gt-0.8:mr-6 bg-background-light inset-shadow-[0_0_0.4rem_black]">
<div class="flex flex-col overflow-y-auto">
<fieldset>
- <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
+ <legend class="font-bold w-full text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
<div class="flex flex-col ar-lt-0.8:grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense">
<div class="m-1">
<label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label>
@@ -17,7 +17,7 @@
</div>
</fieldset>
<fieldset class="mt-1">
- <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
+ <legend class="font-bold w-full text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
<div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col">
<div class="m-1">
<label><input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-a-z"></i></label>