...
1 package server
2
3 import (
4 "log"
5 "slices"
6 "sort"
7 "strconv"
8 "strings"
9 "time"
10
11 "codeberg.org/tslocum/sriracha/internal/database"
12 . "codeberg.org/tslocum/sriracha/model"
13 . "codeberg.org/tslocum/sriracha/util"
14 "github.com/leonelquinteros/gotext"
15 )
16
17
18
19 type notification struct {
20 subscriptionID int
21 postID int
22 mentioned bool
23 }
24
25 func (s *Server) queueNotifications(db *database.DB, p *Post) {
26 if s.config.MailAddress == "" {
27 return
28 }
29
30 var references []int
31 for _, m := range RefLinkPattern.FindAllStringSubmatch(p.Message, -1) {
32 if len(m) != 2 {
33 continue
34 }
35 id, err := strconv.Atoi(m[1])
36 if err == nil && !slices.Contains(references, id) {
37 references = append(references, id)
38 }
39 }
40
41 notified := make(map[string]bool)
42 for _, referenceID := range references {
43 reference := db.PostByID(referenceID)
44 if reference == nil {
45 continue
46 }
47 subs := db.SubscriptionsByPost(reference, true, false)
48 for _, sub := range subs {
49 if notified[sub.Email] {
50 continue
51 }
52 n := notification{
53 subscriptionID: sub.ID,
54 postID: p.ID,
55 mentioned: true,
56 }
57 s.notifications = append(s.notifications, n)
58 notified[sub.Email] = true
59 }
60 }
61
62 subs := db.SubscriptionsByPost(p, true, true)
63 for _, sub := range subs {
64 if notified[sub.Email] {
65 continue
66 }
67 n := notification{
68 subscriptionID: sub.ID,
69 postID: p.ID,
70 }
71 s.notifications = append(s.notifications, n)
72 }
73 }
74
75 func (s *Server) sendNotifications(onlyMentions bool) {
76 if len(s.notifications) == 0 {
77 return
78 }
79 db := s.begin()
80 defer db.Commit()
81
82 type notificationInfo struct {
83 n notification
84 p *Post
85 }
86
87 var keep []notification
88 var modified bool
89 postCache := make(map[int]*Post)
90 pending := make(map[string][]*notificationInfo)
91 for _, n := range s.notifications {
92 sub := db.SubscriptionByID(n.subscriptionID)
93 if sub == nil {
94 modified = true
95 continue
96 } else if onlyMentions && !n.mentioned {
97 keep = append(keep, n)
98 continue
99 }
100 modified = true
101 post, ok := postCache[n.postID]
102 if !ok {
103 post = db.PostByID(n.postID)
104 postCache[n.postID] = post
105 }
106 if post == nil {
107 continue
108 }
109 pending[sub.Email] = append(pending[sub.Email], ¬ificationInfo{n: n, p: post})
110 }
111 if modified {
112 client, err := s.connectToMailServer()
113 if err != nil {
114 log.Fatalf("failed to send notifications: %s", err)
115 }
116 const batchSize = 16
117 var sent int
118 for email, allInfo := range pending {
119 sort.Slice(allInfo, func(i, j int) bool {
120 if allInfo[i].n.mentioned != allInfo[j].n.mentioned {
121 return allInfo[i].n.mentioned
122 } else if allInfo[i].p.Board.ID != allInfo[j].p.Board.ID {
123 return allInfo[i].p.Board.Name < allInfo[j].p.Board.Name
124 }
125 return allInfo[i].p.ID < allInfo[j].p.ID
126 })
127
128 var message strings.Builder
129 var lastBoard int
130 var i int
131 var mentioned bool
132 var lastMentioned bool
133 for _, info := range allInfo {
134 if lastMentioned && !info.n.mentioned {
135 message.WriteString("\n\n===\n\n")
136 lastBoard = 0
137 }
138
139 p := info.p
140 if p.Board.ID != lastBoard {
141 if lastBoard != 0 {
142 message.WriteString("\n\n")
143 }
144 message.WriteString(p.Board.Path())
145
146 i = 0
147 lastBoard = p.Board.ID
148 }
149
150 message.WriteString("\n" + string(p.URL(s.opt.SiteHome)))
151 if info.n.mentioned {
152 message.WriteString(" ***")
153 mentioned = true
154 }
155 i++
156 lastMentioned = info.n.mentioned
157 }
158
159 key := md5Sum(s.hashData(md5Sum(email)))
160 message.WriteString("\n\n--\n" + gotext.Get("Manage Subscriptions") + "\n" + s.opt.SiteHome + "sriracha/subscribe/?email=" + email + "&key=" + key)
161
162 l := len(allInfo)
163 subject := gotext.GetN("%d new post", "%d new posts", l, l)
164 if mentioned {
165 subject = gotext.Get("(Mentioned) %s", subject)
166 }
167
168 if sent == batchSize {
169 client.Close()
170 client, err = s.connectToMailServer()
171 if err != nil {
172 log.Fatalf("failed to send notifications: %s", err)
173 }
174 sent = 0
175 }
176
177 err := s.sendMail(client, email, subject, message.String())
178 if err != nil {
179 log.Fatalf("failed to send email: %s", err)
180 }
181 sent++
182 }
183 client.Close()
184
185 s.notifications = keep
186 }
187 }
188
189 func (s *Server) handleNotifications() {
190 defer s.notificationsWaitGroup.Done()
191
192 mentionTicker := time.NewTicker(time.Duration(s.config.Mentions) * time.Minute)
193 defaultTicker := time.NewTicker(time.Duration(s.config.Notifications) * time.Minute)
194 for {
195 select {
196 case <-mentionTicker.C:
197 s.sendNotifications(true)
198 case <-defaultTicker.C:
199 s.sendNotifications(false)
200 case <-s.shutdownNotifications:
201 s.sendNotifications(false)
202 return
203 }
204 }
205 }
206
View as plain text