github.com/safing/portbase@v0.19.5/notifications/notification.go (about)

     1  package notifications
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/safing/portbase/database/record"
    10  	"github.com/safing/portbase/log"
    11  	"github.com/safing/portbase/modules"
    12  	"github.com/safing/portbase/utils"
    13  )
    14  
    15  // Type describes the type of a notification.
    16  type Type uint8
    17  
    18  // Notification types.
    19  const (
    20  	Info    Type = 0
    21  	Warning Type = 1
    22  	Prompt  Type = 2
    23  	Error   Type = 3
    24  )
    25  
    26  // State describes the state of a notification.
    27  type State string
    28  
    29  // NotificationActionFn defines the function signature for notification action
    30  // functions.
    31  type NotificationActionFn func(context.Context, *Notification) error
    32  
    33  // Possible notification states.
    34  // State transitions can only happen from top to bottom.
    35  const (
    36  	// Active describes a notification that is active, no expired and,
    37  	// if actions are available, still waits for the user to select an
    38  	// action.
    39  	Active State = "active"
    40  	// Responded describes a notification where the user has already
    41  	// selected which action to take but that action is still to be
    42  	// performed.
    43  	Responded State = "responded"
    44  	// Executes describes a notification where the user has selected
    45  	// and action and that action has been performed.
    46  	Executed State = "executed"
    47  )
    48  
    49  // Notification represents a notification that is to be delivered to the user.
    50  type Notification struct { //nolint:maligned
    51  	record.Base
    52  	// EventID is used to identify a specific notification. It consists of
    53  	// the module name and a per-module unique event id.
    54  	// The following format is recommended:
    55  	// 	<module-id>:<event-id>
    56  	EventID string
    57  	// GUID is a unique identifier for each notification instance. That is
    58  	// two notifications with the same EventID must still have unique GUIDs.
    59  	// The GUID is mainly used for system (Windows) integration and is
    60  	// automatically populated by the notification package. Average users
    61  	// don't need to care about this field.
    62  	GUID string
    63  	// Type is the notification type. It can be one of Info, Warning or Prompt.
    64  	Type Type
    65  	// Title is an optional and very short title for the message that gives a
    66  	// hint about what the notification is about.
    67  	Title string
    68  	// Category is an optional category for the notification that allows for
    69  	// tagging and grouping notifications by category.
    70  	Category string
    71  	// Message is the default message shown to the user if no localized version
    72  	// of the notification is available. Note that the message should already
    73  	// have any paramerized values replaced.
    74  	Message string
    75  	// ShowOnSystem specifies if the notification should be also shown on the
    76  	// operating system. Notifications shown on the operating system level are
    77  	// more focus-intrusive and should only be used for important notifications.
    78  	// If the configuration option "Desktop Notifications" is switched off, this
    79  	// will be forced to false on the first save.
    80  	ShowOnSystem bool
    81  	// EventData contains an additional payload for the notification. This payload
    82  	// may contain contextual data and may be used by a localization framework
    83  	// to populate the notification message template.
    84  	// If EventData implements sync.Locker it will be locked and unlocked together with the
    85  	// notification. Otherwise, EventData is expected to be immutable once the
    86  	// notification has been saved and handed over to the notification or database package.
    87  	EventData interface{}
    88  	// Expires holds the unix epoch timestamp at which the notification expires
    89  	// and can be cleaned up.
    90  	// Users can safely ignore expired notifications and should handle expiry the
    91  	// same as deletion.
    92  	Expires int64
    93  	// State describes the current state of a notification. See State for
    94  	// a list of available values and their meaning.
    95  	State State
    96  	// AvailableActions defines a list of actions that a user can choose from.
    97  	AvailableActions []*Action
    98  	// SelectedActionID is updated to match the ID of one of the AvailableActions
    99  	// based on the user selection.
   100  	SelectedActionID string
   101  
   102  	// belongsTo holds the module this notification belongs to. The notification
   103  	// lifecycle will be mirrored to the module's failure status.
   104  	belongsTo *modules.Module
   105  
   106  	lock           sync.Mutex
   107  	actionFunction NotificationActionFn // call function to process action
   108  	actionTrigger  chan string          // and/or send to a channel
   109  	expiredTrigger chan struct{}        // closed on expire
   110  }
   111  
   112  // Action describes an action that can be taken for a notification.
   113  type Action struct {
   114  	// ID specifies a unique ID for the action. If an action is selected, the ID
   115  	// is written to SelectedActionID and the notification is saved.
   116  	// If the action type is not ActionTypeNone, the ID may be empty, signifying
   117  	// that this action is merely additional and selecting it does not dismiss the
   118  	// notification.
   119  	ID string
   120  	// Text on the button.
   121  	Text string
   122  	// Type specifies the action type. Implementing interfaces should only
   123  	// display action types they can handle.
   124  	Type ActionType
   125  	// Payload holds additional data for special action types.
   126  	Payload interface{}
   127  }
   128  
   129  // ActionType defines a specific type of action.
   130  type ActionType string
   131  
   132  // Action Types.
   133  const (
   134  	ActionTypeNone        = ""             // Report selected ID back to backend.
   135  	ActionTypeOpenURL     = "open-url"     // Open external URL
   136  	ActionTypeOpenPage    = "open-page"    // Payload: Page ID
   137  	ActionTypeOpenSetting = "open-setting" // Payload: See struct definition below.
   138  	ActionTypeOpenProfile = "open-profile" // Payload: Scoped Profile ID
   139  	ActionTypeInjectEvent = "inject-event" // Payload: Event ID
   140  	ActionTypeWebhook     = "call-webhook" // Payload: See struct definition below.
   141  )
   142  
   143  // ActionTypeOpenSettingPayload defines the payload for the OpenSetting Action Type.
   144  type ActionTypeOpenSettingPayload struct {
   145  	// Key is the key of the setting.
   146  	Key string
   147  	// Profile is the scoped ID of the profile.
   148  	// Leaving this empty opens the global settings.
   149  	Profile string
   150  }
   151  
   152  // ActionTypeWebhookPayload defines the payload for the WebhookPayload Action Type.
   153  type ActionTypeWebhookPayload struct {
   154  	// HTTP Method to use. Defaults to "GET", or "POST" if a Payload is supplied.
   155  	Method string
   156  	// URL to call.
   157  	// If the URL is relative, prepend the current API endpoint base path.
   158  	// If the URL is absolute, send request to the Portmaster.
   159  	URL string
   160  	// Payload holds arbitrary payload data.
   161  	Payload interface{}
   162  	// ResultAction defines what should be done with successfully returned data.
   163  	// Must one of:
   164  	// - `ignore`: do nothing (default)
   165  	// - `display`: the result is a human readable message, display it in a success message.
   166  	ResultAction string
   167  }
   168  
   169  // Get returns the notification identifed by the given id or nil if it doesn't exist.
   170  func Get(id string) *Notification {
   171  	notsLock.RLock()
   172  	defer notsLock.RUnlock()
   173  	n, ok := nots[id]
   174  	if ok {
   175  		return n
   176  	}
   177  	return nil
   178  }
   179  
   180  // Delete deletes the notification with the given id.
   181  func Delete(id string) {
   182  	// Delete notification in defer to enable deferred unlocking.
   183  	var n *Notification
   184  	var ok bool
   185  	defer func() {
   186  		if ok {
   187  			n.Delete()
   188  		}
   189  	}()
   190  
   191  	notsLock.Lock()
   192  	defer notsLock.Unlock()
   193  	n, ok = nots[id]
   194  }
   195  
   196  // NotifyInfo is a helper method for quickly showing an info notification.
   197  // The notification will be activated immediately.
   198  // If the provided id is empty, an id will derived from msg.
   199  // ShowOnSystem is disabled.
   200  // If no actions are defined, a default "OK" (ID:"ack") action will be added.
   201  func NotifyInfo(id, title, msg string, actions ...Action) *Notification {
   202  	return notify(Info, id, title, msg, false, actions...)
   203  }
   204  
   205  // NotifyWarn is a helper method for quickly showing a warning notification
   206  // The notification will be activated immediately.
   207  // If the provided id is empty, an id will derived from msg.
   208  // ShowOnSystem is enabled.
   209  // If no actions are defined, a default "OK" (ID:"ack") action will be added.
   210  func NotifyWarn(id, title, msg string, actions ...Action) *Notification {
   211  	return notify(Warning, id, title, msg, true, actions...)
   212  }
   213  
   214  // NotifyError is a helper method for quickly showing an error notification.
   215  // The notification will be activated immediately.
   216  // If the provided id is empty, an id will derived from msg.
   217  // ShowOnSystem is enabled.
   218  // If no actions are defined, a default "OK" (ID:"ack") action will be added.
   219  func NotifyError(id, title, msg string, actions ...Action) *Notification {
   220  	return notify(Error, id, title, msg, true, actions...)
   221  }
   222  
   223  // NotifyPrompt is a helper method for quickly showing a prompt notification.
   224  // The notification will be activated immediately.
   225  // If the provided id is empty, an id will derived from msg.
   226  // ShowOnSystem is disabled.
   227  // If no actions are defined, a default "OK" (ID:"ack") action will be added.
   228  func NotifyPrompt(id, title, msg string, actions ...Action) *Notification {
   229  	return notify(Prompt, id, title, msg, false, actions...)
   230  }
   231  
   232  func notify(nType Type, id, title, msg string, showOnSystem bool, actions ...Action) *Notification {
   233  	// Process actions.
   234  	var acts []*Action
   235  	if len(actions) == 0 {
   236  		// Create ack action if there are no defined actions.
   237  		acts = []*Action{
   238  			{
   239  				ID:   "ack",
   240  				Text: "OK",
   241  			},
   242  		}
   243  	} else {
   244  		// Reference given actions for notification.
   245  		acts = make([]*Action, len(actions))
   246  		for index := range actions {
   247  			a := actions[index]
   248  			acts[index] = &a
   249  		}
   250  	}
   251  
   252  	return Notify(&Notification{
   253  		EventID:          id,
   254  		Type:             nType,
   255  		Title:            title,
   256  		Message:          msg,
   257  		ShowOnSystem:     showOnSystem,
   258  		AvailableActions: acts,
   259  	})
   260  }
   261  
   262  // Notify sends the given notification.
   263  func Notify(n *Notification) *Notification {
   264  	// While this function is very similar to Save(), it is much nicer to use in
   265  	// order to just fire off one notification, as it does not require some more
   266  	// uncommon Go syntax.
   267  
   268  	n.save(true)
   269  	return n
   270  }
   271  
   272  // Save saves the notification.
   273  func (n *Notification) Save() {
   274  	n.save(true)
   275  }
   276  
   277  // save saves the notification to the internal storage. It locks the
   278  // notification, so it must not be locked when save is called.
   279  func (n *Notification) save(pushUpdate bool) {
   280  	var id string
   281  
   282  	// Save notification after pre-save processing.
   283  	defer func() {
   284  		if id != "" {
   285  			// Lock and save to notification storage.
   286  			notsLock.Lock()
   287  			defer notsLock.Unlock()
   288  			nots[id] = n
   289  		}
   290  	}()
   291  
   292  	// We do not access EventData here, so it is enough to just lock the
   293  	// notification itself.
   294  	n.lock.Lock()
   295  	defer n.lock.Unlock()
   296  
   297  	// Check if required data is present.
   298  	if n.Title == "" && n.Message == "" {
   299  		log.Warning("notifications: ignoring notification without Title or Message")
   300  		return
   301  	}
   302  
   303  	// Derive EventID from Message if not given.
   304  	if n.EventID == "" {
   305  		n.EventID = fmt.Sprintf(
   306  			"unknown:%s",
   307  			utils.DerivedInstanceUUID(n.Message).String(),
   308  		)
   309  	}
   310  
   311  	// Save ID for deletion
   312  	id = n.EventID
   313  
   314  	// Generate random GUID if not set.
   315  	if n.GUID == "" {
   316  		n.GUID = utils.RandomUUID(n.EventID).String()
   317  	}
   318  
   319  	// Make sure we always have a notification state assigned.
   320  	if n.State == "" {
   321  		n.State = Active
   322  	}
   323  
   324  	// Initialize on first save.
   325  	if !n.KeyIsSet() {
   326  		// Set database key.
   327  		n.SetKey(fmt.Sprintf("notifications:all/%s", n.EventID))
   328  
   329  		// Check if notifications should be shown on the system at all.
   330  		if !useSystemNotifications() {
   331  			n.ShowOnSystem = false
   332  		}
   333  	}
   334  
   335  	// Update meta data.
   336  	n.UpdateMeta()
   337  
   338  	// Push update via the database system if needed.
   339  	if pushUpdate {
   340  		log.Tracef("notifications: pushing update for %s to subscribers", n.Key())
   341  		dbController.PushUpdate(n)
   342  	}
   343  }
   344  
   345  // SetActionFunction sets a trigger function to be executed when the user reacted on the notification.
   346  // The provided function will be started as its own goroutine and will have to lock everything it accesses, even the provided notification.
   347  func (n *Notification) SetActionFunction(fn NotificationActionFn) *Notification {
   348  	n.lock.Lock()
   349  	defer n.lock.Unlock()
   350  	n.actionFunction = fn
   351  	return n
   352  }
   353  
   354  // Response waits for the user to respond to the notification and returns the selected action.
   355  func (n *Notification) Response() <-chan string {
   356  	n.lock.Lock()
   357  	defer n.lock.Unlock()
   358  
   359  	if n.actionTrigger == nil {
   360  		n.actionTrigger = make(chan string)
   361  	}
   362  
   363  	return n.actionTrigger
   364  }
   365  
   366  // Update updates/resends a notification if it was not already responded to.
   367  func (n *Notification) Update(expires int64) {
   368  	// Save when we're finished, if needed.
   369  	save := false
   370  	defer func() {
   371  		if save {
   372  			n.save(true)
   373  		}
   374  	}()
   375  
   376  	n.lock.Lock()
   377  	defer n.lock.Unlock()
   378  
   379  	// Don't update if notification isn't active.
   380  	if n.State != Active {
   381  		return
   382  	}
   383  
   384  	// Don't update too quickly.
   385  	if n.Meta().Modified > time.Now().Add(-10*time.Second).Unix() {
   386  		return
   387  	}
   388  
   389  	// Update expiry and save.
   390  	n.Expires = expires
   391  	save = true
   392  }
   393  
   394  // Delete (prematurely) cancels and deletes a notification.
   395  func (n *Notification) Delete() {
   396  	// Dismiss notification.
   397  	func() {
   398  		n.lock.Lock()
   399  		defer n.lock.Unlock()
   400  
   401  		if n.actionTrigger != nil {
   402  			close(n.actionTrigger)
   403  			n.actionTrigger = nil
   404  		}
   405  	}()
   406  
   407  	n.delete(true)
   408  }
   409  
   410  // delete deletes the notification from the internal storage. It locks the
   411  // notification, so it must not be locked when delete is called.
   412  func (n *Notification) delete(pushUpdate bool) {
   413  	var id string
   414  
   415  	// Delete notification after processing deletion.
   416  	defer func() {
   417  		// Lock and delete from notification storage.
   418  		notsLock.Lock()
   419  		defer notsLock.Unlock()
   420  		delete(nots, id)
   421  	}()
   422  
   423  	// We do not access EventData here, so it is enough to just lock the
   424  	// notification itself.
   425  	n.lock.Lock()
   426  	defer n.lock.Unlock()
   427  
   428  	// Save ID for deletion
   429  	id = n.EventID
   430  
   431  	// Mark notification as deleted.
   432  	n.Meta().Delete()
   433  
   434  	// Close expiry channel if available.
   435  	if n.expiredTrigger != nil {
   436  		close(n.expiredTrigger)
   437  		n.expiredTrigger = nil
   438  	}
   439  
   440  	// Push update via the database system if needed.
   441  	if pushUpdate {
   442  		dbController.PushUpdate(n)
   443  	}
   444  
   445  	n.resolveModuleFailure()
   446  }
   447  
   448  // Expired notifies the caller when the notification has expired.
   449  func (n *Notification) Expired() <-chan struct{} {
   450  	n.lock.Lock()
   451  	defer n.lock.Unlock()
   452  
   453  	if n.expiredTrigger == nil {
   454  		n.expiredTrigger = make(chan struct{})
   455  	}
   456  
   457  	return n.expiredTrigger
   458  }
   459  
   460  // selectAndExecuteAction sets the user response and executes/triggers the action, if possible.
   461  func (n *Notification) selectAndExecuteAction(id string) {
   462  	if n.State != Active {
   463  		return
   464  	}
   465  
   466  	n.State = Responded
   467  	n.SelectedActionID = id
   468  
   469  	executed := false
   470  	if n.actionFunction != nil {
   471  		module.StartWorker("notification action execution", func(ctx context.Context) error {
   472  			return n.actionFunction(ctx, n)
   473  		})
   474  		executed = true
   475  	}
   476  
   477  	if n.actionTrigger != nil {
   478  		// satisfy all listeners (if they are listening)
   479  		// TODO(ppacher): if we miss to notify the waiter here (because
   480  		//                nobody is listeing on actionTrigger) we wil likely
   481  		//                never be able to execute the action again (simply because
   482  		//                we won't try). May consider replacing the single actionTrigger
   483  		//                channel with a per-listener (buffered) one so we just send
   484  		//                the value and close the channel.
   485  	triggerAll:
   486  		for {
   487  			select {
   488  			case n.actionTrigger <- n.SelectedActionID:
   489  				executed = true
   490  			case <-time.After(100 * time.Millisecond): // mitigate race conditions
   491  				break triggerAll
   492  			}
   493  		}
   494  	}
   495  
   496  	if executed {
   497  		n.State = Executed
   498  		n.resolveModuleFailure()
   499  	}
   500  }
   501  
   502  // Lock locks the Notification. If EventData is set and
   503  // implements sync.Locker it is locked as well. Users that
   504  // want to replace the EventData on a notification must
   505  // ensure to unlock the current value on their own. If the
   506  // new EventData implements sync.Locker as well, it must
   507  // be locked prior to unlocking the notification.
   508  func (n *Notification) Lock() {
   509  	n.lock.Lock()
   510  	if locker, ok := n.EventData.(sync.Locker); ok {
   511  		locker.Lock()
   512  	}
   513  }
   514  
   515  // Unlock unlocks the Notification and the EventData, if
   516  // it implements sync.Locker. See Lock() for more information
   517  // on how to replace and work with EventData.
   518  func (n *Notification) Unlock() {
   519  	n.lock.Unlock()
   520  	if locker, ok := n.EventData.(sync.Locker); ok {
   521  		locker.Unlock()
   522  	}
   523  }