...

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.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], &notificationInfo{n: n, p: post})
   110  	}
   111  	if !modified {
   112  		return // No pending notifications.
   113  	}
   114  
   115  	// batchSize is the maximum number of emails to send using a single SMTP connection.
   116  	// This is required because some SMTP servers limit the number of messages that may
   117  	// be sent at once. When the batch size is reached, a fresh SMTP connection is obtained.
   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  		// Reconnect when batch size is reached.
   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