safing-portbase/notifications/notification.go
Patrick Pacher 1810855f64
Drop support for peristent notifications. Fixes #71
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)
2020-09-14 11:21:23 +02:00

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
}