Files
vikunja/pkg/notifications/notification.go
kolaente f5385c574e feat(websocket): add notification event with XORM AfterInsert dispatch
Add NotificationCreatedEvent that fires automatically when a
DatabaseNotification is inserted, using XORM's AfterInsertProcessor
interface. The AfterInsert hook dispatches the event after the row
is persisted, without callers needing to manage DispatchOnCommit or
DispatchPending.

The WebSocket listener subscribes to this event, reloads the
notification from the database (ensuring accurate timestamps), and
pushes it to connected clients subscribed to the notification.created
event. Dispatch errors are logged rather than propagated since the
DB notification is already committed at that point.
2026-04-02 16:30:23 +00:00

145 lines
3.8 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package notifications
import (
"encoding/json"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"xorm.io/xorm"
)
// Notification is a notification which can be sent via mail or db.
type Notification interface {
ToMail(lang string) *Mail
ToDB() interface{}
Name() string
}
type SubjectID interface {
SubjectID() int64
}
type NotificationWithSubject interface {
Notification
SubjectID
}
type ThreadID interface {
ThreadID() string
}
// Notifiable is an entity which can be notified. Usually a user.
type Notifiable interface {
// RouteForMail should return the email address this notifiable has.
RouteForMail() (string, error)
// RouteForDB should return the id of the notifiable entity to save it in the database.
RouteForDB() int64
// ShouldNotify provides a last-minute way to cancel a notification. It will be called immediately before
// sending a notification. An optional session can be passed to reuse an existing transaction.
ShouldNotify(sessions ...*xorm.Session) (should bool, err error)
// Lang provides the language which should be used for translations in the mail.
Lang() string
}
// Notify notifies a notifiable of a notification.
// An optional xorm session can be passed to reuse an existing transaction for the DB notification.
func Notify(notifiable Notifiable, notification Notification, sessions ...*xorm.Session) (err error) {
if isUnderTest {
sentTestNotifications = append(sentTestNotifications, notification)
return nil
}
should, err := notifiable.ShouldNotify(sessions...)
if err != nil || !should {
log.Debugf("Not notifying user %d because they are disabled", notifiable.RouteForDB())
return err
}
err = notifyMail(notifiable, notification)
if err != nil {
return
}
var s *xorm.Session
if len(sessions) > 0 && sessions[0] != nil {
s = sessions[0]
}
return notifyDB(notifiable, notification, s)
}
func notifyMail(notifiable Notifiable, notification Notification) error {
mail := notification.ToMail(notifiable.Lang())
if mail == nil {
return nil
}
to, err := notifiable.RouteForMail()
if err != nil {
return err
}
mail.To(to)
if threadID, is := notification.(ThreadID); is {
mail.ThreadID(threadID.ThreadID())
}
return SendMail(mail, notifiable.Lang())
}
func notifyDB(notifiable Notifiable, notification Notification, existingSession *xorm.Session) (err error) {
dbContent := notification.ToDB()
if dbContent == nil {
return nil
}
content, err := json.Marshal(dbContent)
if err != nil {
return err
}
dbNotification := &DatabaseNotification{
NotifiableID: notifiable.RouteForDB(),
Notification: json.RawMessage(content),
Name: notification.Name(),
}
if subject, is := notification.(SubjectID); is {
dbNotification.SubjectID = subject.SubjectID()
}
if existingSession != nil {
_, err = existingSession.Insert(dbNotification)
return err
}
s := db.NewSession()
defer s.Close()
_, err = s.Insert(dbNotification)
if err != nil {
_ = s.Rollback()
return err
}
return s.Commit()
}