summaryrefslogtreecommitdiffci
diff options
from:
to:
context:
space:
mode:
-rw-r--r--go.mod5
-rw-r--r--go.sum9
-rw-r--r--internal/b2/client.go7
-rw-r--r--internal/blogtrigger/blogtrigger.go68
-rw-r--r--internal/mailer/mailer.go103
-rw-r--r--internal/router/api-v1-email-send-verification-code.go2
-rw-r--r--internal/router/api-v1-general-page-body.go4
-rw-r--r--locale/localization.en.yaml5
-rw-r--r--locale/localization.go9
-rw-r--r--locale/localization.ru.yaml3
-rw-r--r--main.go14
-rw-r--r--views/layouts/general-mail.html41
-rw-r--r--views/layouts/general-page.html6
-rw-r--r--views/messages/new-post.html20
-rw-r--r--views/pages/global-map.html2
-rw-r--r--views/partials/catalogue-blog-cards.html2
16 files changed, 271 insertions, 29 deletions
diff --git a/go.mod b/go.mod
index 6b86f6b..4ea7ede 100644
--- a/go.mod
+++ b/go.mod
@@ -19,18 +19,21 @@ require (
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
+ github.com/go-co-op/gocron/v2 v2.17.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
- github.com/stretchr/testify v1.10.0 // indirect
+ github.com/robfig/cron/v3 v3.0.1 // indirect
+ github.com/stretchr/testify v1.11.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.65.0 // indirect
github.com/wneessen/go-mail v0.7.2 // indirect
diff --git a/go.sum b/go.sum
index a49ee2c..dd213f1 100644
--- a/go.sum
+++ b/go.sum
@@ -13,6 +13,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
+github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4=
+github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -32,6 +34,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
+github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
@@ -51,10 +55,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
@@ -78,5 +86,6 @@ golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/b2/client.go b/internal/b2/client.go
index 7b745a8..d712736 100644
--- a/internal/b2/client.go
+++ b/internal/b2/client.go
@@ -3,6 +3,7 @@ package b2
import (
"context"
"fmt"
+ "slices"
"strings"
"time"
@@ -34,6 +35,7 @@ func NewB2Client(cfg *config.B2Config) (*B2Client, error) {
type BlogPage struct {
Link string
FileName string
+ Lang string
Metadata *frontmatter.Metadata
}
@@ -74,6 +76,9 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) {
nameParts := strings.Split(linkParts[len(linkParts)-1], ".")
fileName := strings.Join(nameParts[:len(linkParts)-1], ".")
+ tags := strings.Split(attrs.Info["tags"], ",")
+ slices.Sort(tags)
+
filePaths = append(filePaths, &BlogPage{
Link: obj.Name(),
FileName: fileName,
@@ -83,7 +88,7 @@ func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) {
ActionDate: attrs.Info["action-date"],
PublishedTime: publishedTime,
Thumbnail: attrs.Info["thumbnail"],
- Tags: strings.Split(attrs.Info["tags"], ","),
+ Tags: tags,
Geolocation: attrs.Info["geolocation"],
},
})
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
+}
diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go
index ba2202e..f51186e 100644
--- a/internal/mailer/mailer.go
+++ b/internal/mailer/mailer.go
@@ -7,11 +7,13 @@ import (
"encoding/binary"
"fmt"
"log/slog"
+ "slices"
"strconv"
"strings"
"sync"
"time"
+ "github.com/SayaAndy/saya-today-web/internal/b2"
"github.com/SayaAndy/saya-today-web/internal/templatemanager"
"github.com/SayaAndy/saya-today-web/locale"
"github.com/dgraph-io/ristretto/v2"
@@ -149,16 +151,14 @@ func (m *Mailer) MailIsTaken(email string) (bool, error) {
return isTaken, nil
}
-func (m *Mailer) GetInfo(userId string) (email string, lang string, err error) {
- hash := m.GetHash(userId)
-
+func (m *Mailer) GetInfo(userIdHash []byte) (email string, lang string, err error) {
tx, err := m.db.Begin()
if err != nil {
return "", "", fmt.Errorf("failed to initialize transaction with db: %s", err)
}
var rows *sql.Rows
- if rows, err = tx.Query(`SELECT email, lang FROM user_email_table WHERE user_id=? LIMIT 1;`, hash); err != nil {
+ 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)
}
@@ -330,6 +330,7 @@ func (m *Mailer) Subscribe(userId string, subscriptionType SubscriptionType, tag
return fmt.Errorf("failed to initialize transaction with db: %s", err)
}
+ slices.Sort(tags)
tagsOutput := ""
switch subscriptionType {
case All:
@@ -357,6 +358,98 @@ func (m *Mailer) Subscribe(userId string, subscriptionType SubscriptionType, tag
return nil
}
-func SendNewPost() {
+func (m *Mailer) NewPost(post *b2.BlogPage) error {
+ tx, err := m.db.Begin()
+ if err != nil {
+ return fmt.Errorf("failed to initialize transaction with db: %s", err)
+ }
+
+ var rows *sql.Rows
+ if rows, err = tx.Query(`SELECT user_id, tags FROM subscription_user_to_tags_table;`); err != nil {
+ tx.Rollback()
+ return fmt.Errorf("failed to query user-to-tags table in db: %s", err)
+ }
+
+ var userId []byte
+ usersToSend := make([]string, 0)
+ var tagsString string
+ i := -1
+
+rowLoop:
+ for rows.Next() {
+ i++
+ if err = rows.Scan(&userId, &tagsString); err != nil {
+ slog.Warn("failed to scan a row in user-to-tags table", slog.String("error", err.Error()), slog.Int("index", i), slog.String("user_id", base64.RawStdEncoding.EncodeToString(userId)))
+ continue
+ }
+
+ if tagsString == "" {
+ 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, email)
+ continue
+ }
+
+ pageTags := strings.Split(tagsString, ",")
+ for _, tag := range pageTags {
+ if _, found := slices.BinarySearch(post.Metadata.Tags, tag); found {
+ usersToSend = append(usersToSend, email)
+ continue rowLoop
+ }
+ }
+ }
+
+ tx.Commit()
+ rows.Close()
+
+ 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 err != nil {
+ return fmt.Errorf("failed to render message body: %w", err)
+ }
+
+ message := mail.NewMsg()
+
+ if err := message.EnvelopeFrom(m.mailAddress); err != nil {
+ return fmt.Errorf("failed to set ENVELOPE FROM address: %w", err)
+ }
+ 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); err != nil {
+ return fmt.Errorf("failed to set TO address: %w", err)
+ }
+
+ message.SetMessageID()
+ message.SetDate()
+ message.SetBulk()
+ message.Subject(m.l[post.Lang].Mail.NewPost.Subject)
+ message.SetBodyString(mail.TypeTextHTML, string(msgBody))
+
+ messages = append(messages, message)
+ }
+
+ if err := m.mailClient.DialAndSend(messages...); err != nil {
+ return fmt.Errorf("failed to send new post notifications: %w", err)
+ }
+ return nil
}
diff --git a/internal/router/api-v1-email-send-verification-code.go b/internal/router/api-v1-email-send-verification-code.go
index 2cde1bd..9372eb3 100644
--- a/internal/router/api-v1-email-send-verification-code.go
+++ b/internal/router/api-v1-email-send-verification-code.go
@@ -58,7 +58,7 @@ func Api_V1_Email_SendVerificationCode(l map[string]*locale.LocaleConfig) func(c
})
}
- if previousEmail, _, _ := Mailer.GetInfo(id); previousEmail == email {
+ if previousEmail, _, _ := Mailer.GetInfo(Mailer.GetHash(id)); previousEmail == email {
return api_v1_email_sendStatusHtml(c, "email-message", l, "Failed", fiber.ErrUnprocessableEntity.Code, lang, l[lang].UserProfile.EmailAlreadyValidated, map[string]string{})
}
diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go
index 66f1234..b5567b7 100644
--- a/internal/router/api-v1-general-page-body.go
+++ b/internal/router/api-v1-general-page-body.go
@@ -14,6 +14,7 @@ import (
"github.com/SayaAndy/saya-today-web/config"
"github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/internal/blogtrigger"
"github.com/SayaAndy/saya-today-web/internal/factgiver"
"github.com/SayaAndy/saya-today-web/internal/frontmatter"
"github.com/SayaAndy/saya-today-web/internal/mailer"
@@ -24,6 +25,7 @@ import (
var FactGiver *factgiver.FactGiver
var Mailer *mailer.Mailer
+var BlogTrigger *blogtrigger.BlogTriggerScheduler
func init() {
assert(0, tm.Add("general-page-body", "views/partials/general-page-body.html"))
@@ -110,7 +112,7 @@ func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []config.A
} else if len(pathParts) == 2 && pathParts[1] == "user" {
values["Title"] = l[lang].UserProfile.Header
- email, _, err := Mailer.GetInfo(c.IP())
+ email, _, err := Mailer.GetInfo(Mailer.GetHash(c.IP()))
if err != nil {
slog.Error("get info from mailer about a client", slog.String("error", err.Error()))
}
diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml
index ce334d9..8981c49 100644
--- a/locale/localization.en.yaml
+++ b/locale/localization.en.yaml
@@ -26,6 +26,7 @@ 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.'
VerifyEmail:
Subject: 'Verification Link for SAYA.TODAY is waiting for you!'
Welcome: 'Greetings!'
@@ -34,7 +35,9 @@ Mail:
InputCode: "Or input this code on the personal page:"
IfRandom: 'If you are unsure what this letter this, you can ignore it, and also, if you are interested, you may visit my site :)'
NewPost:
- Subject: 'New post arrived at SAYA.TODAY!'
+ Subject: 'A new post arrived at SAYA.TODAY!'
+ Intro: 'A new post came!'
+ CapturedOn: 'Captured on'
UserProfile:
Header: 'Personal Settings'
EmailHeader: 'E-Mail'
diff --git a/locale/localization.go b/locale/localization.go
index 1028c9e..b3a1613 100644
--- a/locale/localization.go
+++ b/locale/localization.go
@@ -52,8 +52,9 @@ type MetadataConfig struct {
}
type MailConfig struct {
- VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"`
- NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"`
+ UnsubscribeFooter string `yaml:"UnsubscribeFooter" json:"UnsubscribeFooter"`
+ VerifyEmail VerifyEmailConfig `yaml:"VerifyEmail" json:"VerifyEmail"`
+ NewPost NewPostConfig `yaml:"NewPost" json:"NewPost"`
}
type VerifyEmailConfig struct {
@@ -66,7 +67,9 @@ type VerifyEmailConfig struct {
}
type NewPostConfig struct {
- Subject string `yaml:"Subject" json:"Subject"`
+ Subject string `yaml:"Subject" json:"Subject"`
+ CapturedOn string `yaml:"CapturedOn" json:"CapturedOn"`
+ Intro string `yaml:"Intro" json:"Intro"`
}
type UserProfileConfig struct {
diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml
index 8894e22..c09ad16 100644
--- a/locale/localization.ru.yaml
+++ b/locale/localization.ru.yaml
@@ -26,6 +26,7 @@ Metadata:
Published: 'Опубликовано'
Action: 'Время действия'
Mail:
+ UnsubscribeFooter: 'Если данное письмо пришло вам случайно, либо вы хотите отписаться, можете перейти по этой ссылке.'
VerifyEmail:
Subject: 'Ссылка для верификации для подтверждения вашей почты SAYA.TODAY'
Welcome: 'Приветствую!'
@@ -35,6 +36,8 @@ Mail:
IfRandom: 'Если ты не понимаешь, что это за спам, можешь проигнорировать это письмо, а ещё, если интересно, можешь посетить мой сайт :)'
NewPost:
Subject: 'Новый пост на SAYA.TODAY!'
+ Intro: 'Вышел новый пост!'
+ CapturedOn: 'Снималось'
UserProfile:
Header: 'Личные настройки'
EmailHeader: 'E-Mail'
diff --git a/main.go b/main.go
index 5a14bc6..8f9f619 100644
--- a/main.go
+++ b/main.go
@@ -11,6 +11,7 @@ import (
"github.com/SayaAndy/saya-today-web/config"
"github.com/SayaAndy/saya-today-web/internal/b2"
+ "github.com/SayaAndy/saya-today-web/internal/blogtrigger"
"github.com/SayaAndy/saya-today-web/internal/factgiver"
"github.com/SayaAndy/saya-today-web/internal/glightbox"
"github.com/SayaAndy/saya-today-web/internal/mailer"
@@ -152,6 +153,19 @@ func main() {
os.Exit(1)
}
+ 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
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ slog.Error("fail to initialize blog trigger", slog.String("error", err.Error()))
+ os.Exit(1)
+ }
+
app.Get("/", router.Api_V1_GeneralPage(localization, availableLanguages))
app.Get("/:lang<len(2)>", router.Api_V1_GeneralPage(localization, availableLanguages))
app.Get("/:lang/map", router.Lang_Map(localization, availableLanguages, b2Client))
diff --git a/views/layouts/general-mail.html b/views/layouts/general-mail.html
index c73a1ec..8f42c7a 100644
--- a/views/layouts/general-mail.html
+++ b/views/layouts/general-mail.html
@@ -22,7 +22,6 @@
html {
font-family: 'Philosopher', sans-serif;
- color: #343b58;
--noise-image: url(data:image/png;base64,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);
}
@@ -31,12 +30,8 @@ body {
}
a {
- color: #c3caff !important;
text-decoration: underline;
-}
-
-a:hover {
- color: #d1d6ff !important;
+ text-shadow: none !important;
}
.header {
@@ -53,8 +48,8 @@ a:hover {
}
.content {
- margin: 0rem 32px 32px 32px;
- padding: 16px;
+ margin: 0 32px 0 32px;
+ padding: 16px 16px 32px 16px;
font-size: 24px;
text-shadow: -1px 0 #343b58, 0 1px #343b58, 1px 0 #343b58, 0 -1px #343b58;
color: white;
@@ -62,11 +57,35 @@ a:hover {
background-image: var(--noise-image);
}
+.footer {
+ margin: 0;
+ padding: 1px 8px 1px 8px;
+ font-size: 16px;
+ color: white;
+}
+
+.darkened {
+ background-color: #9aa5f4;
+ background-image: var(--noise-image);
+}
+
.outlier {
padding-inline: 4px;
background-color: #6c6e75;
background-image: var(--noise-image);
}
+
+table, th, td {
+ border: 1px solid black;
+}
+
+td {
+ padding: 5px;
+}
+
+img {
+ width: 200px;
+}
</style>
</head>
@@ -75,8 +94,12 @@ a:hover {
saya.today
</div>
- <div id="content" class="content">
+ <div id="content" class="content" style="background-color: #e3e6ff !important;">
{{ block "body" . }}{{ end }}
</div>
+
+ <div id="footer" class="footer darkened">
+ {{ block "footer" . }}{{ end }}
+ </div>
</body>
</html>
diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html
index 48a0f16..a7281e8 100644
--- a/views/layouts/general-page.html
+++ b/views/layouts/general-page.html
@@ -9,7 +9,7 @@
<link href="/output.css" rel="stylesheet">
</head>
-<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-[100dvh] flex flex-row">
+<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh flex flex-row">
<script>
let map = null;
@@ -172,7 +172,7 @@
<div class="grow bg-split-left"></div>
<div class="flex flex-col [@media(max-height:32rem)]:flex-row">
- <div id="sidebar" class="bg-sidebar flex sticky flex-col z-20 w-[100dvw] xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] [@media(max-height:32rem)]:w-[5rem] h-[5rem] [@media(max-height:32rem)]:h-[100dvh] shadow-[0_0_1rem_black]">
+ <div id="sidebar" class="bg-sidebar flex sticky flex-col z-20 w-dvw xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] [@media(max-height:32rem)]:w-[5rem] h-[5rem] [@media(max-height:32rem)]:h-dvh shadow-[0_0_1rem_black]">
<div class="text-[3rem] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1dvw] flex flex-row [@media(max-height:32rem)]:flex-col gap-0 relative z-20">
<a href="./" id="sidebar-back-button" class="w-[5rem] h-[5rem] hidden">
<i onclick="return changeUrl('/../', true);" class="fas fa-circle-left flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i>
@@ -234,7 +234,7 @@
</script>
</div>
- <div class="bg-background-dark @container/main flex z-10 w-[100dvw] xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] grow-0 h-[calc(100dvh-5rem)] [@media(max-height:32rem)]:h-[100dvh] shadow-[0_0_1rem_black]">
+ <div class="bg-background-dark @container/main flex z-10 w-dvw xs:w-[90dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] grow-0 h-[calc(100dvh-5rem)] [@media(max-height:32rem)]:h-dvh shadow-[0_0_1rem_black]">
<div class="bg-background-dark border-r-2 border-dashed border-main-hard"></div>
<div class="max-h-100% flex flex-col grow bg-background-dark relative pt-4 ml-8 pr-4">
diff --git a/views/messages/new-post.html b/views/messages/new-post.html
index 4fbfc02..7b3082a 100644
--- a/views/messages/new-post.html
+++ b/views/messages/new-post.html
@@ -1,3 +1,19 @@
{{ define "body" }}
-<p>Новый пост ёпта.</p>
-{{ end }} \ No newline at end of file
+<p>{{ .L.Mail.NewPost.Intro }}</p>
+<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 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>
+ </tr>
+ <tr>
+ <td class="darkened" style="font-size: 16px">{{ .L.Mail.NewPost.CapturedOn }} {{ .Post.Metadata.ActionDate }}</td>
+ </tr>
+</table>
+{{ end }}
+
+{{ define "footer" }}
+<p>{{ .L.Mail.UnsubscribeFooter }}</p>
+{{ end }}
diff --git a/views/pages/global-map.html b/views/pages/global-map.html
index 8be99bf..306f442 100644
--- a/views/pages/global-map.html
+++ b/views/pages/global-map.html
@@ -12,7 +12,7 @@
<link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css">
</head>
-<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-[100dvh]">
+<body data-theme="ram" class="font-andika text-main-hard overflow-hidden bg-interlocked-hexagons h-dvh">
<script>
function switchTheme(theme) {
diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html
index 9bc5a30..b50f419 100644
--- a/views/partials/catalogue-blog-cards.html
+++ b/views/partials/catalogue-blog-cards.html
@@ -5,7 +5,7 @@
<img onclick="return changeUrl('{{ .ArticleLink }}');" class="w-full h-full aspect-square cursor-pointer object-cover rounded-lg select-none" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp">
</a>
<div class="grow my-0.5 mx-1 flex flex-col justify-center">
- <div class="flex flex-row gap-2 max-w-[100%]">
+ <div class="flex flex-row gap-2 max-w-full">
<a href="{{ .ArticleLink }}" onclick="return changeUrl('{{ .ArticleLink }}');" class="grow text-base cursor-pointer">
<span class="font-extrabold">{{ .Title }}</span>
<span class="font-extrabold select-none text-secondary">//</span>