...
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.opt.Notifications {
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 return
113 }
114
115
116
117
118 const batchSize = 16
119
120 client, err := s.connectToMailServer()
121 if err != nil {
122 log.Fatalf("failed to send notifications: %s", err)
123 }
124 var sent int
125 for email, allInfo := range pending {
126 sort.Slice(allInfo, func(i, j int) bool {
127 if allInfo[i].n.mentioned != allInfo[j].n.mentioned {
128 return allInfo[i].n.mentioned
129 } else if allInfo[i].p.Board.ID != allInfo[j].p.Board.ID {
130 return allInfo[i].p.Board.Name < allInfo[j].p.Board.Name
131 }
132 return allInfo[i].p.ID < allInfo[j].p.ID
133 })
134
135 var message strings.Builder
136 var lastBoard int
137 var i int
138 var mentioned bool
139 var lastMentioned bool
140 for _, info := range allInfo {
141 if lastMentioned && !info.n.mentioned {
142 message.WriteString("\n\n===\n\n")
143 lastBoard = 0
144 }
145
146 p := info.p
147 if p.Board.ID != lastBoard {
148 if lastBoard != 0 {
149 message.WriteString("\n\n")
150 }
151 message.WriteString(p.Board.Path())
152
153 i = 0
154 lastBoard = p.Board.ID
155 }
156
157 message.WriteString("\n" + string(p.URL(s.opt.SiteHome)))
158 if info.n.mentioned {
159 message.WriteString(" ***")
160 mentioned = true
161 }
162 i++
163 lastMentioned = info.n.mentioned
164 }
165
166 key := md5Sum(s.hashData(md5Sum(email)))
167 message.WriteString("\n\n--\n" + gotext.Get("Manage Subscriptions") + "\n" + s.opt.SiteHome + "sriracha/subscribe/?email=" + email + "&key=" + key)
168
169 l := len(allInfo)
170 subject := gotext.GetN("%d new post", "%d new posts", l, l)
171 if mentioned {
172 subject = gotext.Get("(Mentioned) %s", subject)
173 }
174
175
176 if sent == batchSize {
177 client.Close()
178 client, err = s.connectToMailServer()
179 if err != nil {
180 log.Fatalf("failed to send notifications: %s", err)
181 }
182 sent = 0
183 }
184
185 err := s.sendMail(client, email, subject, message.String())
186 if err != nil {
187 log.Fatalf("failed to send email: %s", err)
188 }
189 sent++
190 }
191 client.Close()
192
193 s.notifications = keep
194 }
195
196 func (s *Server) handleNotifications() {
197 defer s.notificationsWaitGroup.Done()
198
199 mentionTicker := time.NewTicker(time.Duration(s.config.Mentions) * time.Minute)
200 defaultTicker := time.NewTicker(time.Duration(s.config.Notifications) * time.Minute)
201 for {
202 select {
203 case <-mentionTicker.C:
204 s.sendNotifications(true)
205 case <-defaultTicker.C:
206 s.sendNotifications(false)
207 case <-s.shutdownNotifications:
208 s.sendNotifications(false)
209 return
210 }
211 }
212 }
213
View as plain text