mirror of
https://github.com/safing/portbase
synced 2025-09-01 18:19:57 +00:00
This path drops support for persistent notifications as they were always broken and not used anyway (see #71). As a result, the locking strategy of the injected notification backend has been updated and should improve database push updates (i.e. the "upd" is now correct and no additional "new" updates with partial data will be sent anymore)
296 lines
6.8 KiB
Go
296 lines
6.8 KiB
Go
package notifications
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/safing/portbase/database/record"
|
|
"github.com/safing/portbase/log"
|
|
"github.com/safing/portbase/utils"
|
|
)
|
|
|
|
// Type describes the type of a notification.
|
|
type Type uint8
|
|
|
|
// Notification types
|
|
const (
|
|
Info Type = 0
|
|
Warning Type = 1
|
|
Prompt Type = 2
|
|
)
|
|
|
|
// Notification represents a notification that is to be delivered to the user.
|
|
type Notification struct {
|
|
record.Base
|
|
|
|
ID string
|
|
GUID string
|
|
|
|
Message string
|
|
// MessageTemplate string
|
|
// MessageData []string
|
|
DataSubject sync.Locker
|
|
Type Type
|
|
|
|
Created int64 // creation timestamp, notification "starts"
|
|
Expires int64 // expiry timestamp, notification is expected to be canceled at this time and may be cleaned up afterwards
|
|
Responded int64 // response timestamp, notification "ends"
|
|
Executed int64 // execution timestamp, notification will be deleted soon
|
|
|
|
AvailableActions []*Action
|
|
SelectedActionID string
|
|
|
|
lock sync.Mutex
|
|
actionFunction func(*Notification) // call function to process action
|
|
actionTrigger chan string // and/or send to a channel
|
|
expiredTrigger chan struct{} // closed on expire
|
|
}
|
|
|
|
// Action describes an action that can be taken for a notification.
|
|
type Action struct {
|
|
ID string
|
|
Text string
|
|
}
|
|
|
|
func noOpAction(n *Notification) {}
|
|
|
|
// Get returns the notification identifed by the given id or nil if it doesn't exist.
|
|
func Get(id string) *Notification {
|
|
notsLock.RLock()
|
|
defer notsLock.RUnlock()
|
|
n, ok := nots[id]
|
|
if ok {
|
|
return n
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NotifyInfo is a helper method for quickly showing a info
|
|
// notification. The notification is already shown. If id is
|
|
// an empty string a new UUIDv4 will be generated.
|
|
func NotifyInfo(id, msg string, actions ...Action) *Notification {
|
|
return notify(Info, id, msg, actions...)
|
|
}
|
|
|
|
// NotifyWarn is a helper method for quickly showing a warning
|
|
// notification. The notification is already shown. If id is
|
|
// an empty string a new UUIDv4 will be generated.
|
|
func NotifyWarn(id, msg string, actions ...Action) *Notification {
|
|
return notify(Warning, id, msg, actions...)
|
|
}
|
|
|
|
// NotifyPrompt is a helper method for quickly showing a prompt
|
|
// notification. The notification is already shown. If id is
|
|
// an empty string a new UUIDv4 will be generated.
|
|
func NotifyPrompt(id, msg string, actions ...Action) *Notification {
|
|
return notify(Prompt, id, msg, actions...)
|
|
}
|
|
|
|
func notify(nType Type, id string, msg string, actions ...Action) *Notification {
|
|
acts := make([]*Action, len(actions))
|
|
for idx := range actions {
|
|
a := actions[idx]
|
|
acts[idx] = &a
|
|
}
|
|
|
|
if id == "" {
|
|
id = utils.DerivedInstanceUUID(msg).String()
|
|
}
|
|
|
|
n := Notification{
|
|
ID: id,
|
|
Message: msg,
|
|
Type: nType,
|
|
AvailableActions: acts,
|
|
}
|
|
|
|
return n.Save()
|
|
}
|
|
|
|
// Save saves the notification and returns it.
|
|
func (n *Notification) Save() *Notification {
|
|
n.Lock()
|
|
defer n.Unlock()
|
|
|
|
return n.save(true)
|
|
}
|
|
|
|
func (n *Notification) save(pushUpdate bool) *Notification {
|
|
// initialize
|
|
if n.Created == 0 {
|
|
n.Created = time.Now().Unix()
|
|
}
|
|
if n.GUID == "" {
|
|
n.GUID = utils.RandomUUID(n.ID).String()
|
|
}
|
|
|
|
// make ack notification if there are no defined actions
|
|
if len(n.AvailableActions) == 0 {
|
|
n.AvailableActions = []*Action{
|
|
{
|
|
ID: "ack",
|
|
Text: "OK",
|
|
},
|
|
}
|
|
n.actionFunction = noOpAction
|
|
}
|
|
|
|
// check key
|
|
if n.DatabaseKey() == "" {
|
|
n.SetKey(fmt.Sprintf("notifications:all/%s", n.ID))
|
|
}
|
|
|
|
n.UpdateMeta()
|
|
|
|
// store the notification inside or map
|
|
notsLock.Lock()
|
|
nots[n.ID] = n
|
|
notsLock.Unlock()
|
|
|
|
if pushUpdate {
|
|
log.Tracef("notifications: pushing update for %s to subscribers", n.Key())
|
|
dbController.PushUpdate(n)
|
|
}
|
|
|
|
return n
|
|
}
|
|
|
|
// SetActionFunction sets a trigger function to be executed when the user reacted on the notification.
|
|
// The provided function will be started as its own goroutine and will have to lock everything it accesses, even the provided notification.
|
|
func (n *Notification) SetActionFunction(fn func(*Notification)) *Notification {
|
|
n.lock.Lock()
|
|
defer n.lock.Unlock()
|
|
n.actionFunction = fn
|
|
return n
|
|
}
|
|
|
|
// Response waits for the user to respond to the notification and returns the selected action.
|
|
func (n *Notification) Response() <-chan string {
|
|
n.lock.Lock()
|
|
defer n.lock.Unlock()
|
|
|
|
if n.actionTrigger == nil {
|
|
n.actionTrigger = make(chan string)
|
|
}
|
|
|
|
return n.actionTrigger
|
|
}
|
|
|
|
// Update updates/resends a notification if it was not already responded to.
|
|
func (n *Notification) Update(expires int64) {
|
|
n.lock.Lock()
|
|
defer n.lock.Unlock()
|
|
|
|
if n.Responded == 0 {
|
|
n.Expires = expires
|
|
n.save(true)
|
|
}
|
|
}
|
|
|
|
// Delete (prematurely) cancels and deletes a notification.
|
|
func (n *Notification) Delete() error {
|
|
notsLock.Lock()
|
|
defer notsLock.Unlock()
|
|
n.Lock()
|
|
defer n.Unlock()
|
|
|
|
// mark as deleted
|
|
n.Meta().Delete()
|
|
|
|
// delete from internal storage
|
|
delete(nots, n.ID)
|
|
|
|
// close expired
|
|
if n.expiredTrigger != nil {
|
|
close(n.expiredTrigger)
|
|
n.expiredTrigger = nil
|
|
}
|
|
|
|
// push update
|
|
dbController.PushUpdate(n)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Expired notifies the caller when the notification has expired.
|
|
func (n *Notification) Expired() <-chan struct{} {
|
|
n.lock.Lock()
|
|
defer n.lock.Unlock()
|
|
|
|
if n.expiredTrigger == nil {
|
|
n.expiredTrigger = make(chan struct{})
|
|
}
|
|
|
|
return n.expiredTrigger
|
|
}
|
|
|
|
// selectAndExecuteAction sets the user response and executes/triggers the action, if possible.
|
|
func (n *Notification) selectAndExecuteAction(id string) {
|
|
// abort if already executed
|
|
if n.Executed != 0 {
|
|
return
|
|
}
|
|
|
|
// set response
|
|
n.Responded = time.Now().Unix()
|
|
n.SelectedActionID = id
|
|
|
|
// execute
|
|
executed := false
|
|
if n.actionFunction != nil {
|
|
go n.actionFunction(n)
|
|
executed = true
|
|
}
|
|
if n.actionTrigger != nil {
|
|
// satisfy all listeners
|
|
triggerAll:
|
|
for {
|
|
select {
|
|
case n.actionTrigger <- n.SelectedActionID:
|
|
executed = true
|
|
case <-time.After(100 * time.Millisecond): // mitigate race conditions
|
|
break triggerAll
|
|
}
|
|
}
|
|
}
|
|
|
|
// save execution time
|
|
if executed {
|
|
n.Executed = time.Now().Unix()
|
|
}
|
|
}
|
|
|
|
// AddDataSubject adds the data subject to the notification. This is the only way how a data subject should be added - it avoids locking problems.
|
|
func (n *Notification) AddDataSubject(ds sync.Locker) {
|
|
n.lock.Lock()
|
|
defer n.lock.Unlock()
|
|
n.DataSubject = ds
|
|
}
|
|
|
|
// Lock locks the Notification and the DataSubject, if available.
|
|
func (n *Notification) Lock() {
|
|
n.lock.Lock()
|
|
if n.DataSubject != nil {
|
|
n.DataSubject.Lock()
|
|
}
|
|
}
|
|
|
|
// Unlock unlocks the Notification and the DataSubject, if available.
|
|
func (n *Notification) Unlock() {
|
|
n.lock.Unlock()
|
|
if n.DataSubject != nil {
|
|
n.DataSubject.Unlock()
|
|
}
|
|
}
|
|
|
|
func duplicateActions(original []*Action) (duplicate []*Action) {
|
|
duplicate = make([]*Action, len(original))
|
|
for _, action := range original {
|
|
duplicate = append(duplicate, &Action{
|
|
ID: action.ID,
|
|
Text: action.Text,
|
|
})
|
|
}
|
|
return
|
|
}
|