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.go145
-rw-r--r--internal/router/api-v1-subs.go2
-rw-r--r--internal/router/client-cache.go18
-rw-r--r--internal/router/lang-user-unsubscribe.go71
-rw-r--r--locale/localization.en.yaml8
-rw-r--r--locale/localization.go23
-rw-r--r--locale/localization.ru.yaml8
-rw-r--r--main.go6
-rw-r--r--views/messages/new-post.html4
-rw-r--r--views/pages/blog-catalogue.html8
-rw-r--r--views/pages/unsubscribe-page.html30
16 files changed, 58 insertions, 305 deletions
diff --git a/config/config.go b/config/config.go
index 1393dd8..d5dab2d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -62,18 +62,13 @@ 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"`
- Trigger TriggerConfig `json:"Trigger" yaml:"trigger" validate:"required"`
-}
-
-type TriggerConfig struct {
- OnNewPost string `json:"OnNewPost" yaml:"onNewPost" validate:"cron,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"`
}
func LoadConfig(path string, config *Config) error {
diff --git a/config/config.local.yaml b/config/config.local.yaml
index e23c808..008fa93 100644
--- a/config/config.local.yaml
+++ b/config/config.local.yaml
@@ -32,8 +32,7 @@ auth:
db:
type: sqlite3
config:
- # 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'
+ dsn: 'file:/tmp/auth.db?cache=shared&mode=rwc'
salt: '123'
mail:
clientHost: '127.0.0.1:3000'
@@ -43,5 +42,3 @@ 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 0bee79d..4c0f425 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&_journal_mode=WAL'
+ dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2'
salt: '${AUTH_SALT}'
mail:
clientHost: '${FQDN}'
@@ -42,5 +42,3 @@ 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 1c15dae..003ec05 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&_journal_mode=WAL'
+ dsn: 'file:/data/auth.db?cache=private&mode=rwc&_locking_mode=EXCLUSIVE&_mutex=no&_auto_vacuum=2'
salt: '${AUTH_SALT}'
mail:
clientHost: '${FQDN}'
@@ -42,5 +42,3 @@ 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 c5b3097..ab66981 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, cron string, onTrigger func([]*b2.BlogPage) error) (*BlogTriggerScheduler, 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)
@@ -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(cron, false), gocron.NewTask(func(bts *BlogTriggerScheduler) {
+ 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()))
@@ -49,10 +49,6 @@ 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 d6dbd17..f51186e 100644
--- a/internal/mailer/mailer.go
+++ b/internal/mailer/mailer.go
@@ -6,7 +6,6 @@ import (
"encoding/base64"
"encoding/binary"
"fmt"
- "html/template"
"log/slog"
"slices"
"strconv"
@@ -25,7 +24,6 @@ import (
type Mailer struct {
verificationCodes *ristretto.Cache[uint64, string]
- unsubscribeCodes *ristretto.Cache[uint64, []byte]
db *sql.DB
tm *templatemanager.TemplateManager
mailClient *mail.Client
@@ -60,17 +58,7 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string
NumCounters: 10000,
MaxCost: 1 << 20, // 1 MB
BufferItems: 64,
- TtlTickerDurationInSec: 3600, // 1 hour
- })
- if err != nil {
- return nil, fmt.Errorf("fail to initialize cache for verification codes: %w", err)
- }
-
- unsubscribeCodes, err := ristretto.NewCache(&ristretto.Config[uint64, []byte]{
- NumCounters: 10000,
- MaxCost: 1 << 20, // 1 MB
- BufferItems: 64,
- TtlTickerDurationInSec: 86400, // 1 day
+ TtlTickerDurationInSec: 3600,
})
if err != nil {
return nil, fmt.Errorf("fail to initialize cache for verification codes: %w", err)
@@ -97,7 +85,6 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string
return &Mailer{
verificationCodes: verificationCodes,
- unsubscribeCodes: unsubscribeCodes,
db: db,
clientHost: clientHost,
tm: tm,
@@ -152,20 +139,12 @@ 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()
@@ -178,19 +157,12 @@ 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() {
@@ -203,24 +175,6 @@ func (m *Mailer) GetInfo(userIdHash []byte) (email string, lang string, err erro
return
}
-func (m *Mailer) Unsubscribe(unsubscribeCodeString string) (clientError error, serverError error) {
- unsubscribeCode, err := strconv.ParseUint(unsubscribeCodeString, 16, 64)
- if err != nil {
- return fmt.Errorf("invalid unsubscribe code: %s", err), nil
- }
-
- userId, _ := m.unsubscribeCodes.Get(unsubscribeCode)
- if len(userId) == 0 {
- return fmt.Errorf("invalid unsubscribe code: have no information about it"), nil
- }
-
- if err = m.Subscribe(userId, None); err != nil {
- return nil, fmt.Errorf("failed to unsubscribe: %s", err)
- }
-
- return nil, nil
-}
-
func (m *Mailer) SendVerificationCode(userId string, address string, lang string) error {
message := mail.NewMsg()
@@ -317,22 +271,18 @@ 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])
@@ -345,14 +295,6 @@ 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
@@ -360,6 +302,7 @@ 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() {
@@ -381,14 +324,12 @@ func (m *Mailer) GetSubscriptions(userId string) (subscriptionType SubscriptionT
}
}
-func (m *Mailer) Subscribe(userIdHash []byte, subscriptionType SubscriptionType, tags ...string) error {
+func (m *Mailer) Subscribe(userId string, subscriptionType SubscriptionType, tags ...string) error {
tx, err := m.db.Begin()
if err != nil {
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 {
@@ -400,20 +341,19 @@ func (m *Mailer) Subscribe(userIdHash []byte, subscriptionType SubscriptionType,
tagsOutput = strings.Join(tags, ",")
}
+ hash := m.GetHash(userId)
+
if _, err = tx.Exec(`INSERT INTO subscription_user_to_tags_table(user_id, tags) VALUES(?, ?)
ON CONFLICT(user_id) DO UPDATE SET
- tags=excluded.tags;`, userIdHash, tagsOutput); err != nil {
+ tags=excluded.tags;`, hash, 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
}
@@ -424,19 +364,14 @@ 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)
}
var userId []byte
- usersToSend := make([]struct {
- userId []byte
- email string
- }, 0)
+ usersToSend := make([]string, 0)
var tagsString string
i := -1
@@ -452,21 +387,25 @@ 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, ""})
+ usersToSend = append(usersToSend, email)
continue
}
pageTags := strings.Split(tagsString, ",")
for _, tag := range pageTags {
if _, found := slices.BinarySearch(post.Metadata.Tags, tag); found {
- usersToSend = append(usersToSend, struct {
- userId []byte
- email string
- }{userId, ""})
+ usersToSend = append(usersToSend, email)
continue rowLoop
}
}
@@ -474,42 +413,16 @@ 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
- }
+ msgBody, err := m.tm.Render("new-post", fiber.Map{
+ "L": m.l[post.Lang],
+ "Lang": post.Lang,
+ "Post": post,
+ "ClientHost": m.clientHost,
+ })
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)
-
- unsubscribeFooter := strings.Replace(m.l[post.Lang].Mail.UnsubscribeFooter, "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1)
- unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 1)
-
- msgBody, err := m.tm.Render("new-post", fiber.Map{
- "L": m.l[post.Lang],
- "Lang": post.Lang,
- "Post": post,
- "ClientHost": m.clientHost,
- "UnsubscribeFooter": template.HTML(unsubscribeFooter),
- })
-
if err != nil {
return fmt.Errorf("failed to render message body: %w", err)
}
@@ -522,7 +435,7 @@ rowLoop:
if err := message.FromFormat(m.publicName, m.mailAddress); err != nil {
return fmt.Errorf("failed to set formatted FROM address: %w", err)
}
- if err := message.To(user.email); err != nil {
+ if err := message.To(user); err != nil {
return fmt.Errorf("failed to set TO address: %w", err)
}
@@ -532,8 +445,6 @@ rowLoop:
message.Subject(m.l[post.Lang].Mail.NewPost.Subject)
message.SetBodyString(mail.TypeTextHTML, string(msgBody))
- m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40)
-
messages = append(messages, message)
}
diff --git a/internal/router/api-v1-subs.go b/internal/router/api-v1-subs.go
index 72f16e0..8995ebe 100644
--- a/internal/router/api-v1-subs.go
+++ b/internal/router/api-v1-subs.go
@@ -58,7 +58,7 @@ func Api_V1_Subs_Put(l map[string]*locale.LocaleConfig) func(c *fiber.Ctx) error
}
specificTags := c.FormValue("tags_picked")
- if err = Mailer.Subscribe(Mailer.GetHash(id), subscriptionTypeEnum, specificTags); err != nil {
+ if err = Mailer.Subscribe(id, subscriptionTypeEnum, specificTags); err != nil {
return api_v1_email_sendStatusHtml(c, "subs-message", l, "Failed", fiber.StatusUnprocessableEntity, lang, l[lang].UserProfile.FailedToSubscribe, map[string]string{})
}
diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go
index e7cda30..d607cdd 100644
--- a/internal/router/client-cache.go
+++ b/internal/router/client-cache.go
@@ -31,12 +31,10 @@ 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)
}
@@ -64,7 +62,6 @@ 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)
}
@@ -84,11 +81,8 @@ 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),
@@ -105,28 +99,18 @@ 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)
}
- 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
+ return tx.Commit()
}
func (c *ClientCache) GetHash(id string) string {
diff --git a/internal/router/lang-user-unsubscribe.go b/internal/router/lang-user-unsubscribe.go
deleted file mode 100644
index c39f5e7..0000000
--- a/internal/router/lang-user-unsubscribe.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package router
-
-import (
- "fmt"
- "html/template"
- "log/slog"
-
- "github.com/SayaAndy/saya-today-web/config"
- "github.com/SayaAndy/saya-today-web/locale"
- "github.com/gofiber/fiber/v2"
-)
-
-func init() {
- assert(0, tm.Add("unsubscribe-page", "views/pages/unsubscribe-page.html"))
-}
-
-func Lang_User_Unsubscribe(l map[string]*locale.LocaleConfig, langs []config.AvailableLanguageConfig) func(c *fiber.Ctx) error {
- return func(c *fiber.Ctx) error {
- lang := c.Params("lang")
- for _, availableLang := range langs {
- if availableLang.Name == lang {
- goto langIsAvailable
- }
- }
- return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language", lang))
-
- langIsAvailable:
- var statusEmoji, statusText, statusColor string
- var status int
-
- unsubscribeCode := c.FormValue("code")
- if unsubscribeCode == "" {
- statusColor = "0, 0, 255"
- statusEmoji = "(╭ರ_•́)"
- 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"
- statusEmoji = "♡⸜(˶˃ ᵕ ˂˶)⸝♡"
- statusText = l[lang].UnsubscribePage.Success
- status = fiber.StatusOK
- }
-
- content, err := tm.Render("unsubscribe-page", fiber.Map{
- "Lang": lang,
- "L": l[lang],
- "StatusEmoji": statusEmoji,
- "StatusText": statusText,
- "StatusColor": template.HTML(statusColor),
- })
- if err != nil {
- slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/user/unsubscribe"), slog.String("error", err.Error()))
- c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
- return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate page")
- }
-
- return c.Type("html").Status(status).Send(content)
- }
-}
diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml
index c7f2df8..8981c49 100644
--- a/locale/localization.en.yaml
+++ b/locale/localization.en.yaml
@@ -22,17 +22,11 @@ HomePage:
Hymn2: "We've been expecting you!"
Hymn3: 'You bring such joy in Sayasite!'
Hymn4: 'No matter where you roam, know our love is true!~'
-UnsubscribePage:
- Header: 'Cancelling the Subscription'
- UnsetCode: 'Your cancel code is not set. You just having a walk around the site?'
- InvalidCode: "Your cancel code could not be verified. Maybe, the code expired after a day. Or maybe, you've already unsubscribed."
- OnServerError: "We couldn't unsubscribe you, the problem is on our side! We will solve this problem as soon as possible."
- Success: 'Cancelled your subscription successfully! We do hope we will engage you again someday.'
Metadata:
Published: 'Published'
Action: 'Took place on'
Mail:
- UnsubscribeFooter: 'If this letter got you in a bad mood, you can unsubscribe from my blog by {}this link{/}.'
+ UnsubscribeFooter: 'If this letter got you in a bad mood, you can unsubscribe from my blog by this link.'
VerifyEmail:
Subject: 'Verification Link for SAYA.TODAY is waiting for you!'
Welcome: 'Greetings!'
diff --git a/locale/localization.go b/locale/localization.go
index c6467c4..b3a1613 100644
--- a/locale/localization.go
+++ b/locale/localization.go
@@ -8,14 +8,13 @@ import (
)
type LocaleConfig struct {
- TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
- BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
- GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"`
- HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"`
- UnsubscribePage UnsubscribePageConfig `yaml:"UnsubscribePage" json:"UnsubscribePage"`
- Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"`
- Mail MailConfig `yaml:"Mail" json:"Mail"`
- UserProfile UserProfileConfig `yaml:"UserProfile" json:"UserProfile"`
+ TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"`
+ BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"`
+ GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"`
+ HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"`
+ Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"`
+ Mail MailConfig `yaml:"Mail" json:"Mail"`
+ UserProfile UserProfileConfig `yaml:"UserProfile" json:"UserProfile"`
}
type BlogSearchConfig struct {
@@ -47,14 +46,6 @@ type HomePageConfig struct {
Hymn4 string `yaml:"Hymn4" json:"Hymn4"`
}
-type UnsubscribePageConfig struct {
- Header string `yaml:"Header" json:"Header"`
- UnsetCode string `yaml:"UnsetCode" json:"UnsetCode"`
- InvalidCode string `yaml:"InvalidCode" json:"InvalidCode"`
- OnServerError string `yaml:"OnServerError" json:"OnServerError"`
- Success string `yaml:"Success" json:"Success"`
-}
-
type MetadataConfig struct {
Action string `yaml:"Action" json:"Action"`
Published string `yaml:"Published" json:"Published"`
diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml
index 5df82da..c09ad16 100644
--- a/locale/localization.ru.yaml
+++ b/locale/localization.ru.yaml
@@ -22,17 +22,11 @@ HomePage:
Hymn2: 'Мы Вас столько ждали!'
Hymn3: 'Столько радости от Вас на Саясайте!'
Hymn4: 'Где бы Вы не были, знайте, -- наша любовь крепка!'
-UnsubscribePage:
- Header: 'Отписка'
- UnsetCode: 'Код для отписки не проставлен. Вы случайно не заблудились?'
- InvalidCode: 'Код для отписки не подтверждён. Может, истекло время в сутки для кода. А может, вы уже успели отписаться.'
- OnServerError: 'Мы не смогли вас отписать, проблема на нашей стороне! В скором времени займёмся проблемой.'
- Success: 'Вы отписались от рассылки! Надеюсь, мы ещё сможем вас заинтересовать.'
Metadata:
Published: 'Опубликовано'
Action: 'Время действия'
Mail:
- UnsubscribeFooter: 'Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по {}этой ссылке{/}.'
+ UnsubscribeFooter: 'Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по этой ссылке.'
VerifyEmail:
Subject: 'Ссылка для верификации для подтверждения вашей почты SAYA.TODAY'
Welcome: 'Приветствую!'
diff --git a/main.go b/main.go
index 174854d..8f9f619 100644
--- a/main.go
+++ b/main.go
@@ -153,7 +153,7 @@ func main() {
os.Exit(1)
}
- router.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(b2Client, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost, func(bp []*b2.BlogPage) error {
+ router.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(b2Client, cfg.AvailableLanguages, func(bp []*b2.BlogPage) error {
for _, post := range bp {
if err := router.Mailer.NewPost(post); err != nil {
return err
@@ -170,7 +170,6 @@ func main() {
app.Get("/:lang<len(2)>", router.Api_V1_GeneralPage(localization, availableLanguages))
app.Get("/:lang/map", router.Lang_Map(localization, availableLanguages, b2Client))
app.Get("/:lang/user", router.Api_V1_GeneralPage(localization, availableLanguages))
- app.Get("/:lang/user/unsubscribe", router.Lang_User_Unsubscribe(localization, availableLanguages))
app.Get("/:lang/blog", router.Api_V1_GeneralPage(localization, availableLanguages))
app.Get("/:lang/blog/:title", router.Api_V1_GeneralPage(localization, availableLanguages))
@@ -202,9 +201,6 @@ 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/messages/new-post.html b/views/messages/new-post.html
index f1338f7..7b3082a 100644
--- a/views/messages/new-post.html
+++ b/views/messages/new-post.html
@@ -3,7 +3,7 @@
<table>
<tr>
<td rowspan="3"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}"><img src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Post.Metadata.Thumbnail }}.webp"></a></td>
- <td class="darkened" style="font-size: 24px"><a style="color: #273de1 !important;" href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}">{{ .Post.Metadata.Title }}</a></td>
+ <td class="darkened" style="font-size: 24px"><a href="https://{{ .ClientHost }}/{{ .Lang }}/blog/{{ .Post.FileName }}">{{ .Post.Metadata.Title }}</a></td>
</tr>
<tr>
<td class="darkened" style="font-size: 16px">{{ .Post.Metadata.ShortDescription }}</td>
@@ -15,5 +15,5 @@
{{ end }}
{{ define "footer" }}
-<p>{{ .UnsubscribeFooter }}</p>
+<p>{{ .L.Mail.UnsubscribeFooter }}</p>
{{ end }}
diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html
index d209205..8e91798 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-full">
+<div class="flex flex-row ar-lt-0.8:flex-col text-base h-[100%]">
<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-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]">
+ 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]">
<div class="flex flex-col overflow-y-auto">
<fieldset>
- <legend class="font-bold w-full text-center">{{ .L.BlogSearch.TagsHeader }}</legend>
+ <legend class="font-bold w-[100%] 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-full text-center">{{ .L.BlogSearch.OrderByHeader }}</legend>
+ <legend class="font-bold w-[100%] 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>
diff --git a/views/pages/unsubscribe-page.html b/views/pages/unsubscribe-page.html
deleted file mode 100644
index 3876818..0000000
--- a/views/pages/unsubscribe-page.html
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>{{ .L.UnsubscribePage.Header }} // SAYA TODAY</title>
-<style>
- html {
- --noise-image: url(data:image/png;base64,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);
- }
-</style>
-</head>
-
-<body style="width: 100dvw; height: 100dvh; display: flex; flex-direction: column; position: fixed">
-
- <div style='flex: 3 0 0; background-image: var(--noise-image); background-color: rgba({{ .StatusColor }}, 0.1)'></div>
-
- <div style="flex: 1 0 0; background-image: var(--noise-image); align-content: center;">
- <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 2rem;">{{ .StatusEmoji }}</p>
- </div>
-
- <div style="flex: 1 0 0; background-image: var(--noise-image); align-content: center;">
- <p style="position: relative; width: 100%; margin-inline: auto; text-align: center; font-size: 1.5rem;">{{ .StatusText }}</p>
- </div>
-
- <div style='flex: 3 0 0; background-image: var(--noise-image); background-color: rgba({{ .StatusColor }}, 0.2)'></div>
-
-</body>
-
-</html>