...

Source file src/codeberg.org/tslocum/sriracha/internal/server/server_notification.go

Documentation: codeberg.org/tslocum/sriracha/internal/server

     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  // notification represents a pending notification. The referenced subscription
    18  // and post are refreshed before the sending the notification.
    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], &notificationInfo{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