noti.m (10977B)
1 //go:build darwin && cgo 2 3 #import <stdlib.h> 4 #import "_cgo_export.h" 5 6 #if ! __has_feature(objc_arc) 7 #error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). 8 #endif 9 10 @import Foundation; 11 @import UserNotifications; 12 13 // Log levels mirror Go's slog package. 14 const int DEBUG = -4; 15 const int INFO = 0; 16 const int WARN = 4; 17 const int ERROR = 8; 18 19 // notify_log macro provides a var-args wrapper that can do formatting to the Go side log function. 20 #define notify_log(level, fmt, ...) golog(level, [[NSString stringWithFormat:@fmt, ##__VA_ARGS__] UTF8String]) 21 22 @interface NotifyUNDelegate : NSObject <UNUserNotificationCenterDelegate> 23 { } 24 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler; 25 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler; 26 - (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(UNNotification *)notification; 27 @end 28 29 @implementation NotifyUNDelegate 30 - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { 31 notify_log(DEBUG, "NotifyUNDelegate: didReceiveNotificationResponse %@", response); 32 33 NSError *error; 34 NSData *jsonData = [NSJSONSerialization dataWithJSONObject: response.notification.request.content.userInfo options: NSJSONWritingPrettyPrinted error: &error]; 35 NSString *actionID = response.actionIdentifier; 36 NSString *categoryID = response.notification.request.content.categoryIdentifier; 37 NSString *userText = @""; 38 39 // NOTE(jfm): macOS passes a different object for text actions (UNTextInputNotificationResponse). 40 // Since we declare the method to use UNNotificationResponse, we must use reflection to grab the 41 // userText property when the concrete object is actually UNTextInputNotificationResponse. 42 // In the case of a non-text action the userText will be the empty string. 43 44 @try { 45 userText = [response valueForKey:@"userText"]; 46 } 47 @catch (NSException *e) { 48 // ignore 49 } 50 51 // Category, Action, UserText, UserData (as JSON data). 52 gocallback([categoryID UTF8String], [actionID UTF8String], [userText UTF8String], [[[NSString alloc] initWithBytes:[jsonData bytes] length:[jsonData length] encoding:NSUTF8StringEncoding] UTF8String]); 53 54 completionHandler(); 55 } 56 57 - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { 58 notify_log(DEBUG, "NotifyUNDelegate: willPresentNotification"); 59 } 60 61 - (void)userNotificationCenter:(UNUserNotificationCenter *)center openSettingsForNotification:(UNNotification *)notification { 62 notify_log(DEBUG, "NotifyUNDelegate: openSettingsForNotification"); 63 } 64 @end 65 66 UNUserNotificationCenter *nc; 67 NotifyUNDelegate *del; 68 BOOL enabled; 69 BOOL hasBundle; 70 71 // noti_setup registers the app delegate. 72 void 73 noti_setup() { 74 notify_log(DEBUG, "Getting application bundle"); 75 enabled = NO; 76 hasBundle = NO; 77 NSBundle *main = [NSBundle mainBundle]; 78 if (main.bundleIdentifier == nil) { 79 notify_log(ERROR, "No app bundle."); 80 return; 81 } 82 hasBundle = YES; 83 notify_log(DEBUG, "Bundle ID: %@", main.bundleIdentifier); 84 nc = [UNUserNotificationCenter currentNotificationCenter]; 85 del = [NotifyUNDelegate new]; 86 nc.delegate = del; 87 } 88 89 // noti_category_register adds a notification category. 90 void 91 noti_category_register(Category category) { 92 NSString *category_id = [[NSString alloc] initWithUTF8String:category.id]; 93 NSString *preview = [[NSString alloc] initWithUTF8String:category.preview]; 94 NSString *summary = [[NSString alloc] initWithUTF8String:category.summary]; 95 96 if (!hasBundle) { 97 return; 98 } 99 100 NSMutableArray *actions_array = [NSMutableArray new]; 101 102 /* 103 Iterate both types of actions and add them to the notification center. 104 */ 105 106 for (uintptr_t ii = 0; ii < category.actions_len; ii++) { 107 Action a = category.actions[ii]; 108 109 NSString *a_id = [[NSString alloc] initWithUTF8String:a.id]; 110 NSString *a_icon = [[NSString alloc] initWithUTF8String:a.icon]; 111 NSString *a_title = [[NSString alloc] initWithUTF8String:a.title]; 112 113 NSString *a_icon_path = [[NSBundle mainBundle] pathForResource:a_icon.stringByDeletingPathExtension ofType:a_icon.pathExtension]; 114 115 [actions_array addObject:[UNNotificationAction actionWithIdentifier:a_id title:a_title options:0 icon:[UNNotificationActionIcon iconWithTemplateImageName:a_icon_path]]]; 116 } 117 118 for (uintptr_t ii = 0; ii < category.text_input_actions_len; ii++) { 119 TextInputAction tia = category.text_input_actions[ii]; 120 121 NSString *tia_id = [[NSString alloc] initWithUTF8String:tia.id]; 122 NSString *tia_icon = [[NSString alloc] initWithUTF8String:tia.icon]; 123 NSString *tia_title = [[NSString alloc] initWithUTF8String:tia.title]; 124 NSString *tia_placeholder = [[NSString alloc] initWithUTF8String:tia.placeholder]; 125 NSString *tia_button_title = [[NSString alloc] initWithUTF8String:tia.button_title]; 126 127 NSString *tia_icon_path = [[NSBundle mainBundle] pathForResource:tia_icon.stringByDeletingPathExtension ofType:tia_icon.pathExtension]; 128 129 [actions_array addObject:[UNTextInputNotificationAction actionWithIdentifier:tia_id title:tia_title options:UNNotificationActionOptionForeground icon:[UNNotificationActionIcon iconWithTemplateImageName:tia_icon_path] textInputButtonTitle:tia_button_title textInputPlaceholder:tia_placeholder]]; 130 } 131 132 UNNotificationCategory *un_category = [UNNotificationCategory categoryWithIdentifier:category_id actions:actions_array intentIdentifiers:[NSArray new] hiddenPreviewsBodyPlaceholder:preview categorySummaryFormat:summary options:0]; 133 134 135 // We must take care to merge the categories into a single set 136 // otherwise we will overwrite the existing categories with this 137 // one. 138 139 UNUserNotificationCenter *nc = [UNUserNotificationCenter currentNotificationCenter]; 140 141 [nc getNotificationCategoriesWithCompletionHandler: ^(NSSet<UNNotificationCategory *> * categories) { 142 NSMutableSet *set = [NSMutableSet setWithSet:categories]; 143 [set addObject:un_category]; 144 [nc setNotificationCategories:set]; 145 }]; 146 147 return; 148 } 149 150 // noti_notify fires a notification. 151 void 152 noti_notify(Notification n) { 153 if (!hasBundle) { 154 notify_log(ERROR, "no bundle"); 155 return; 156 } 157 158 notify_log(DEBUG, "creating notification"); 159 160 dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 161 162 UNMutableNotificationContent *note = [[UNMutableNotificationContent alloc] init]; 163 164 note.title = [[NSString alloc] initWithUTF8String: n.title]; 165 note.subtitle = [[NSString alloc] initWithUTF8String: n.sub_title]; 166 note.body = [[NSString alloc] initWithUTF8String: n.body]; 167 note.categoryIdentifier = [[NSString alloc] initWithUTF8String: n.category_id]; 168 169 /* 170 171 Process attachments. 172 173 */ 174 175 NSMutableArray *attachments_array = [NSMutableArray new]; 176 177 for (uintptr_t ii = 0; ii < n.attachments_len; ii++) { 178 179 NSError *error; 180 NSString *attachment_id = @""; // id will be generated for us. 181 NSString *attachment_name = [[NSString alloc] initWithUTF8String:n.attachments[ii]]; 182 NSString *attachment_path = [[NSBundle mainBundle] pathForResource:attachment_name.stringByDeletingPathExtension ofType:attachment_name.pathExtension]; 183 NSURL *url = [NSURL fileURLWithPath:attachment_path?attachment_path:attachment_name]; 184 185 // TODO: add ability to express clipping rectangle options dictionary. 186 UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:attachment_id URL:url options:nil error:&error]; 187 188 notify_log(DEBUG, "attachment: %@", attachment); 189 190 if (attachment) { 191 [attachments_array addObject:attachment]; 192 } else { 193 notify_log(ERROR, "attachment not found for %@", attachment_name); 194 } 195 } 196 197 note.attachments = attachments_array; 198 199 /* 200 201 Process user data. 202 203 */ 204 205 NSError *json_error; 206 NSData *user_data_data = [[[NSString alloc] initWithUTF8String:n.user_data] dataUsingEncoding:NSUTF8StringEncoding]; 207 NSDictionary *user_data_dict = [NSJSONSerialization JSONObjectWithData:user_data_data options:NSJSONReadingFragmentsAllowed error:&json_error]; 208 209 if (json_error) { 210 notify_log(ERROR, "unmarshalling user data into dictionary: %@", json_error); 211 } 212 213 note.userInfo = user_data_dict; 214 215 notify_log(DEBUG, "user info: %@", user_data_dict); 216 217 /* 218 219 Build and register ad-hoc category. 220 This logic kicks in the there is not category id set on the notification 221 and there is one or more actions. 222 223 */ 224 225 if ([note.categoryIdentifier length] == 0 && (n.actions_len > 0 || n.text_input_actions_len > 0)) { 226 notify_log(DEBUG, "creating adhoc category for notification"); 227 228 NSString *adhoc_category_id = [[NSUUID UUID] UUIDString]; 229 230 noti_category_register((Category){ 231 .id = [adhoc_category_id UTF8String], 232 .summary = "adhoc category", 233 .preview = "", 234 .actions = n.actions, 235 .actions_len = n.actions_len, 236 .text_input_actions = n.text_input_actions, 237 .text_input_actions_len = n.text_input_actions_len, 238 }); 239 240 note.categoryIdentifier = adhoc_category_id; 241 242 } 243 244 /* 245 246 Submit request. 247 248 */ 249 250 notify_log(DEBUG, "creating request"); 251 252 UNNotificationRequest *req = [UNNotificationRequest requestWithIdentifier:[NSBundle mainBundle].bundleIdentifier content: note trigger:nil]; 253 254 notify_log(DEBUG, "requesting authorization"); 255 256 [nc requestAuthorizationWithOptions: UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler: ^(BOOL granted, NSError *error){ 257 notify_log(DEBUG, "granted = %s", granted?"true":"false"); 258 259 if (error) { 260 notify_log(ERROR, "authorizing: %@", error); 261 return; 262 } 263 264 enabled = granted; 265 266 if (enabled == YES) { 267 notify_log(DEBUG, "adding notification request"); 268 269 [nc addNotificationRequest:req withCompletionHandler: ^(NSError *error) { 270 if (error) { 271 notify_log(ERROR, "adding notification: error: %@", error); 272 } 273 dispatch_semaphore_signal(semaphore); 274 }]; 275 276 } else { 277 notify_log(ERROR, "permission denied"); 278 dispatch_semaphore_signal(semaphore); 279 } 280 }]; 281 282 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 283 284 return; 285 } 286 287 // noti_cancel cancels the specified notification. 288 void 289 noti_cancel() { 290 if (!hasBundle) { 291 notify_log(ERROR, "no bundle"); 292 return; 293 } 294 295 NSString *nid = [NSBundle mainBundle].bundleIdentifier; 296 297 @try { 298 [nc removePendingNotificationRequestsWithIdentifiers: @[(NSString*)nid]]; 299 [nc removeDeliveredNotificationsWithIdentifiers: @[(NSString*)nid]]; 300 } 301 302 @catch(NSException *ne) { 303 notify_log(ERROR, "caught exception when cancelling notification %@: %@", nid, ne); 304 } 305 } 306