From 4af058bd02ee74d16a8b80de9f54cba12a8c1a1a Mon Sep 17 00:00:00 2001 From: popaprozac Date: Wed, 19 Mar 2025 18:10:59 -0700 Subject: [PATCH] more cleanup --- .../services/notifications/notifications.go | 31 +++++++- .../notifications/notifications_darwin.go | 12 ++- .../notifications/notifications_darwin.m | 75 ++++++++----------- .../notifications/notifications_linux.go | 8 ++ .../notifications/notifications_windows.go | 36 ++++++--- 5 files changed, 103 insertions(+), 59 deletions(-) diff --git a/v3/pkg/services/notifications/notifications.go b/v3/pkg/services/notifications/notifications.go index fb519aed2..d3295d8c1 100644 --- a/v3/pkg/services/notifications/notifications.go +++ b/v3/pkg/services/notifications/notifications.go @@ -1,10 +1,25 @@ +// Package notifications provides cross-platform notification capabilities for desktop applications. +// It supports macOS, Windows, and Linux with a consistent API while handling platform-specific +// differences internally. Key features include: +// - Basic notifications with title, subtitle, and body +// - Interactive notifications with buttons and actions +// - Notification categories for reusing configurations +// - User feedback handling with a unified callback system +// +// Platform-specific notes: +// - macOS: Requires a properly bundled and signed application +// - Windows: Uses Windows Toast notifications +// - Linux: Falls back between D-Bus, notify-send, or other methods and does not support text inputs package notifications -import "sync" +import ( + "fmt" + "sync" +) // Service represents the notifications service type Service struct { - // notificationResponseCallback is called when a notification response is received + // notificationResponseCallback is called when a notification result is received. // Only one callback can be assigned at a time. notificationResultCallback func(result NotificationResult) @@ -93,3 +108,15 @@ func (ns *Service) handleNotificationResult(result NotificationResult) { callback(result) } } + +func validateNotificationOptions(options NotificationOptions) error { + if options.ID == "" { + return fmt.Errorf("notification ID cannot be empty") + } + + if options.Title == "" { + return fmt.Errorf("notification title cannot be empty") + } + + return nil +} diff --git a/v3/pkg/services/notifications/notifications_darwin.go b/v3/pkg/services/notifications/notifications_darwin.go index f17dd5a69..638c8b3a4 100644 --- a/v3/pkg/services/notifications/notifications_darwin.go +++ b/v3/pkg/services/notifications/notifications_darwin.go @@ -56,7 +56,7 @@ func CheckBundleIdentifier() bool { } // RequestNotificationAuthorization requests permission for notifications. -// Default timeout is 5 minutes +// Default timeout is 15 minutes func (ns *Service) RequestNotificationAuthorization() (bool, error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*900) defer cancel() @@ -94,6 +94,10 @@ func (ns *Service) CheckNotificationAuthorization() (bool, error) { // SendNotification sends a basic notification with a unique identifier, title, subtitle, and body. func (ns *Service) SendNotification(options NotificationOptions) error { + if err := validateNotificationOptions(options); err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -139,6 +143,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error { // A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category. // If a NotificationCategory is not registered a basic notification will be sent. func (ns *Service) SendNotificationWithActions(options NotificationOptions) error { + if err := validateNotificationOptions(options); err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -243,7 +251,7 @@ func (ns *Service) RemoveNotificationCategory(categoryId string) error { if result.Error != nil { return result.Error } - return fmt.Errorf("category registration failed") + return fmt.Errorf("category removal failed") } return nil case <-ctx.Done(): diff --git a/v3/pkg/services/notifications/notifications_darwin.m b/v3/pkg/services/notifications/notifications_darwin.m index abb129842..cd1c96898 100644 --- a/v3/pkg/services/notifications/notifications_darwin.m +++ b/v3/pkg/services/notifications/notifications_darwin.m @@ -106,37 +106,43 @@ void checkNotificationAuthorization(int channelID) { }]; } -void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) { - ensureDelegateInitialized(); - - NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; +// Helper function to create notification content +UNMutableNotificationContent* createNotificationContent(const char *title, const char *subtitle, + const char *body, const char *data_json) { NSString *nsTitle = [NSString stringWithUTF8String:title]; - NSString *nsSubtitle = [NSString stringWithUTF8String:subtitle]; + NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @""; NSString *nsBody = [NSString stringWithUTF8String:body]; - NSMutableDictionary *customData = [NSMutableDictionary dictionary]; + UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; + content.title = nsTitle; + if (![nsSubtitle isEqualToString:@""]) { + content.subtitle = nsSubtitle; + } + content.body = nsBody; + content.sound = [UNNotificationSound defaultSound]; + + // Parse JSON data if provided if (data_json) { NSString *dataJsonStr = [NSString stringWithUTF8String:data_json]; NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding]; NSError *error = nil; NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; - if (error) { - NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; - captureResult(channelID, false, [errorMsg UTF8String]); - return; - } - if (parsedData) { - [customData addEntriesFromDictionary:parsedData]; + if (!error && parsedData) { + content.userInfo = parsedData; } } - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + return content; +} + +void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) { + ensureDelegateInitialized(); - UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; - content.title = nsTitle; - content.subtitle = nsSubtitle; - content.body = nsBody; - content.sound = [UNNotificationSound defaultSound]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json); + NSMutableDictionary *customData = [NSMutableDictionary dictionary]; if (customData.count > 0) { content.userInfo = customData; @@ -157,35 +163,16 @@ void sendNotification(int channelID, const char *identifier, const char *title, } void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle, - const char *body, const char *categoryId, const char *actions_json) { + const char *body, const char *categoryId, const char *data_json) { ensureDelegateInitialized(); - NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; - NSString *nsTitle = [NSString stringWithUTF8String:title]; - NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @""; - NSString *nsBody = [NSString stringWithUTF8String:body]; - NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; - - NSMutableDictionary *customData = [NSMutableDictionary dictionary]; - if (actions_json) { - NSString *actionsJsonStr = [NSString stringWithUTF8String:actions_json]; - NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding]; - NSError *error = nil; - NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; - if (!error && parsedData) { - [customData addEntriesFromDictionary:parsedData]; - } - } - UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + + NSString *nsIdentifier = [NSString stringWithUTF8String:identifier]; + NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; + UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json); + NSMutableDictionary *customData = [NSMutableDictionary dictionary]; - UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; - content.title = nsTitle; - if (![nsSubtitle isEqualToString:@""]) { - content.subtitle = nsSubtitle; - } - content.body = nsBody; - content.sound = [UNNotificationSound defaultSound]; content.categoryIdentifier = nsCategoryId; if (customData.count > 0) { diff --git a/v3/pkg/services/notifications/notifications_linux.go b/v3/pkg/services/notifications/notifications_linux.go index 34b110987..a75996f50 100644 --- a/v3/pkg/services/notifications/notifications_linux.go +++ b/v3/pkg/services/notifications/notifications_linux.go @@ -415,6 +415,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error { return errors.New("notification service not initialized") } + if err := validateNotificationOptions(options); err != nil { + return err + } + notifier.Lock() defer notifier.Unlock() @@ -454,6 +458,10 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro return errors.New("notification service not initialized") } + if err := validateNotificationOptions(options); err != nil { + return err + } + notificationLock.RLock() category, exists := notificationCategories[options.CategoryID] notificationLock.RUnlock() diff --git a/v3/pkg/services/notifications/notifications_windows.go b/v3/pkg/services/notifications/notifications_windows.go index 6afda46b4..06884f3f6 100644 --- a/v3/pkg/services/notifications/notifications_windows.go +++ b/v3/pkg/services/notifications/notifications_windows.go @@ -128,6 +128,10 @@ func (ns *Service) CheckNotificationAuthorization() bool { // SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows. // (subtitle and category id are only available on macOS) func (ns *Service) SendNotification(options NotificationOptions) error { + if err := validateNotificationOptions(options); err != nil { + return err + } + if err := saveIconToDir(); err != nil { fmt.Printf("Error saving icon: %v\n", err) } @@ -140,9 +144,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error { if options.Data != nil { encodedPayload, err := encodePayload(DefaultActionIdentifier, options.Data) - if err == nil { - n.ActivationArguments = encodedPayload + if err != nil { + return fmt.Errorf("failed to encode notification data: %w", err) } + n.ActivationArguments = encodedPayload } return n.Push() @@ -153,6 +158,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error { // If a NotificationCategory is not registered a basic notification will be sent. // (subtitle and category id are only available on macOS) func (ns *Service) SendNotificationWithActions(options NotificationOptions) error { + if err := validateNotificationOptions(options); err != nil { + return err + } + if err := saveIconToDir(); err != nil { fmt.Printf("Error saving icon: %v\n", err) } @@ -191,15 +200,15 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro n.ActivationArguments, _ = encodePayload(n.ActivationArguments, options.Data) for index := range n.Actions { - n.Actions[index].Arguments, _ = encodePayload(n.Actions[index].Arguments, options.Data) + encodedPayload, err := encodePayload(n.Actions[index].Arguments, options.Data) + if err != nil { + return fmt.Errorf("failed to encode notification data: %w", err) + } + n.Actions[index].Arguments = encodedPayload } } - err := n.Push() - if err != nil { - return err - } - return nil + return n.Push() } // RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. @@ -344,20 +353,25 @@ func loadCategoriesFromRegistry() error { ) if err != nil { if err == registry.ErrNotExist { + // Not an error, no saved categories return nil } - return err + return fmt.Errorf("failed to open registry key: %w", err) } defer key.Close() data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey) if err != nil { - return err + if err == registry.ErrNotExist { + // No value yet, but key exists + return nil + } + return fmt.Errorf("failed to read categories from registry: %w", err) } categories := make(map[string]NotificationCategory) if err := json.Unmarshal([]byte(data), &categories); err != nil { - return err + return fmt.Errorf("failed to parse notification categories from registry: %w", err) } notificationCategoriesLock.Lock()