├── .ci └── Jenkinsfile ├── .gitignore ├── LICENSE ├── Notifier ├── .swiftlint.yml ├── Notifier - Alerts │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Notifier - 1024x1024.png │ │ │ ├── Notifier - 128x128.png │ │ │ ├── Notifier - 16x16.png │ │ │ ├── Notifier - 256x256 1.png │ │ │ ├── Notifier - 256x256.png │ │ │ ├── Notifier - 32x32 1.png │ │ │ ├── Notifier - 32x32.png │ │ │ ├── Notifier - 512x512 1.png │ │ │ ├── Notifier - 512x512.png │ │ │ └── Notifier - 64x64.png │ │ └── Contents.json │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Functions.swift │ ├── Notifier - Alerts.entitlements │ ├── Structures.swift │ └── UserNotifications.swift ├── Notifier - Notifications │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Notifier - 1024x1024.png │ │ │ ├── Notifier - 128x128.png │ │ │ ├── Notifier - 16x16.png │ │ │ ├── Notifier - 256x256 1.png │ │ │ ├── Notifier - 256x256.png │ │ │ ├── Notifier - 32x32 1.png │ │ │ ├── Notifier - 32x32.png │ │ │ ├── Notifier - 512x512 1.png │ │ │ ├── Notifier - 512x512.png │ │ │ └── Notifier - 64x64.png │ │ └── Contents.json │ ├── Base.lproj │ │ └── MainMenu.xib │ ├── Functions.swift │ ├── Notifier - Notifications.entitlements │ ├── Notifier___Notifications.entitlements │ ├── Structures.swift │ └── UserNotifications.swift ├── Notifier.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcshareddata │ │ └── xcschemes │ │ └── Notifier.xcscheme └── Notifier │ ├── AppDelegate.swift │ ├── ArgParser.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Notifier - 1024x1024.png │ │ ├── Notifier - 128x128.png │ │ ├── Notifier - 16x16.png │ │ ├── Notifier - 256x256 1.png │ │ ├── Notifier - 256x256.png │ │ ├── Notifier - 32x32 1.png │ │ ├── Notifier - 32x32.png │ │ ├── Notifier - 512x512 1.png │ │ ├── Notifier - 512x512.png │ │ └── Notifier - 64x64.png │ └── Contents.json │ ├── Base.lproj │ └── MainMenu.xib │ ├── Functions.swift │ ├── Info.plist │ ├── Notifier.entitlements │ ├── Structures.swift │ └── UserNotifications.swift ├── README.md ├── catalog-info.yaml ├── package └── make-notifier-pkg.sh └── profile └── Allow Notifier Notifications.mobileconfig /.ci/Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env groovy 2 | 3 | @Library('tools') _ 4 | import com.jamf.jenkins.KanikoBuild 5 | import java.text.SimpleDateFormat 6 | 7 | pipeline { 8 | agent { 9 | kubernetes { 10 | defaultContainer 'sonar' 11 | yaml ''' 12 | apiVersion: v1 13 | kind: Pod 14 | spec: 15 | serviceAccountName: ecr-ci-sa 16 | containers: 17 | - name: sonar 18 | image: docker.jamf.build/sonarsource/sonar-scanner-cli:latest 19 | tty: true 20 | command: 21 | - cat 22 | ''' 23 | } 24 | } 25 | options { 26 | buildDiscarder(logRotator(numToKeepStr: '6')) // Keeps only the last 6 builds 27 | } 28 | 29 | environment { 30 | PROJECT = 'Notifier' 31 | SONARPROJECT = "com.jamf.team-red-dawg:Notifier" 32 | SONAR_URL = "https://sonarqube.jamf.build" 33 | } 34 | 35 | stages { 36 | //Sonarqube Analysis 37 | stage('SonarQube Analysis') { 38 | when { 39 | beforeAgent true 40 | anyOf { 41 | branch 'main' 42 | branch 'dev' 43 | not { changeRequest() } 44 | } 45 | } 46 | steps { 47 | container('sonar') { 48 | script { 49 | performSonarQubeAnalysis() 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | post { 57 | always { 58 | publishHTML([ 59 | allowMissing: true, 60 | alwaysLinkToLastBuild: false, 61 | reportDir: 'build/reports/tests/test', 62 | reportFiles: 'index.html', 63 | reportName: 'Gradle Build Report', 64 | keepAll: true 65 | ]) 66 | } 67 | } 68 | } 69 | 70 | 71 | // Define all custom Jenkins utility functions 72 | 73 | // Define a reusable function for time logging 74 | def logCurrentTime(String context = "Time Log") { 75 | def sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z") 76 | echo "${context}: ${sdf.format(new Date())}" 77 | } 78 | 79 | // Define a reusable function Git Setup 80 | def setupGitConfig() { 81 | echo 'Setting up git configuration' 82 | env.GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=no' 83 | sh """ 84 | git config --global user.email "${env.GIT_COMMITTER_EMAIL}" 85 | git config --global user.name "${env.GIT_COMMITTER_NAME}" 86 | """ 87 | } 88 | 89 | // Define a reusable function for repo setup 90 | def cloneRepository(String repoDirectory) { 91 | try { 92 | echo "Cloning repository from ${env.HELM_MANIFEST_REPO} into ${repoDirectory}" 93 | 94 | sh """ 95 | set -x 96 | git clone --single-branch --branch ${env.HELM_REPO_BRANCH} ${env.HELM_MANIFEST_REPO} ${repoDirectory} 97 | """ 98 | 99 | echo "Repository cloned successfully into ${repoDirectory}" 100 | } catch (Exception e) { 101 | echo "Error while cloning repository: ${e.getMessage()}" 102 | throw e // Rethrow the exception to handle it at a higher level or fail the build 103 | } 104 | } 105 | 106 | // Define a reusable function for SonarQube Analysis 107 | def performSonarQubeAnalysis() { 108 | withCredentials([string(credentialsId: 'sonar-token', variable: 'SONAR_AUTH_TOKEN')]) { 109 | def gitUrl = sh(script: 'git config --get remote.origin.url', returnStdout: true).trim() 110 | echo "Git URL: $gitUrl" 111 | 112 | sh ''' 113 | sonar-scanner \ 114 | -Dsonar.host.url="${SONAR_URL}" \ 115 | -Dsonar.login="${SONAR_AUTH_TOKEN}" \ 116 | -Dsonar.branch.name="${BRANCH_NAME}" \ 117 | -Dsonar.projectKey="${SONARPROJECT}" \ 118 | -Dsonar.python.version="3" \ 119 | -Dsonar.sources=. 120 | ''' 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | *.xcuserstate 93 | *.pkg 94 | package/ROOT/Applications/Utilities/* 95 | package/notifier-app.plist 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /Notifier/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - identifier_name 3 | - nesting 4 | - function_parameter_count 5 | opt_in_rules: # some rules are only opt-in 6 | - control_statement 7 | - empty_count 8 | - trailing_newline 9 | - colon 10 | - comma 11 | excluded: # paths to ignore during linting. Takes precedence over `included`. 12 | - Pods 13 | - Scripts/CodeSignUpdate.swift 14 | - Payload 15 | 16 | # configurable rules can be customized from this configuration file 17 | # binary rules can set their severity level 18 | force_cast: warning # implicitly. Give warning only for force casting 19 | 20 | force_try: 21 | severity: warning # explicitly. Give warning only for force try 22 | 23 | type_body_length: 24 | - 300 # warning 25 | - 400 # error 26 | 27 | # or they can set both explicitly 28 | file_length: 29 | warning: 500 30 | error: 800 31 | 32 | large_tuple: # warn user when using 3 values in tuple, give error if there are 4 33 | - 3 34 | - 4 35 | 36 | # naming rules can set warnings/errors for min_length and max_length 37 | # additionally they can set excluded names 38 | type_name: 39 | min_length: 4 # only warning 40 | max_length: # warning and error 41 | warning: 30 42 | error: 35 43 | excluded: iPhone # excluded via string 44 | reporter: "xcode" -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notifier - Alerts 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import Cocoa 10 | import UserNotifications 11 | 12 | // Declaration 13 | @NSApplicationMain 14 | // The apps class 15 | class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { 16 | // IBOutlet declaration 17 | @IBOutlet weak var window: NSWindow! 18 | // When we've finished launching 19 | func applicationDidFinishLaunching(_ aNotification: Notification) { 20 | // If .userInfo ios populated, we've baen launched by interation with a prior posted notification 21 | if let response = (aNotification as NSNotification).userInfo?[ 22 | NSApplication.launchUserNotificationUserInfoKey] as? UNNotificationResponse { 23 | // Handle the notification 24 | handleNotification(forResponse: response) 25 | // Exit 26 | exit(0) 27 | } 28 | // Get the args passed to the binary 29 | let passedCLIArguments = Array(CommandLine.arguments) 30 | // If no args passed, exit 31 | if passedCLIArguments.count == 1 { 32 | // Print error 33 | print("ERROR: No arguments passed to binary, exiting...") 34 | // Exit 35 | exit(0) 36 | // If we have args passed 37 | } else { 38 | // Get the passed base64 string at commandline argument at index 1 39 | let (messageContent, passedBase64, rootElements) = decodeJSON(passedJSON: passedCLIArguments[1]) 40 | // Exit if Notification Center isn't running 41 | isNotificationCenterRunning(verboseMode: rootElements.verboseMode ?? "") 42 | // Ask permission 43 | requestAuthorisation(verboseMode: rootElements.verboseMode ?? "") 44 | // Create a notification center object 45 | let notificationCenter = UNUserNotificationCenter.current() 46 | // Set delegate 47 | notificationCenter.delegate = self 48 | // Process the arguments as needed 49 | processArguments(messageContent: messageContent, notificationCenter: notificationCenter, 50 | passedBase64: passedBase64, passedCLIArguments: passedCLIArguments, 51 | rootElements: rootElements) 52 | } 53 | } 54 | } 55 | 56 | // Process the arguments as needed 57 | func processArguments(messageContent: MessageContent, notificationCenter: UNUserNotificationCenter, 58 | passedBase64: String, passedCLIArguments: [String], rootElements: RootElements) { 59 | // Create a notification content object 60 | let notificationContent = UNMutableNotificationContent() 61 | // Add category identifier to notificationContent required anyway so setting here 62 | notificationContent.categoryIdentifier = "alert" 63 | // If verbose mode is set 64 | if rootElements.verboseMode != nil { 65 | // Add verboseMode to userInfo 66 | notificationContent.userInfo["verboseMode"] = "enabled" 67 | // Progress log 68 | NSLog("\(#function.components(separatedBy: "(")[0]) - verbose enabled") 69 | } 70 | // If we're to remove all delivered notifications 71 | if rootElements.removeOption == "all" { 72 | // Remove all notifications 73 | removeAllPriorNotifications(notificationCenter: notificationCenter, messageContent: messageContent, 74 | rootElements: rootElements) 75 | // If we're not removing 76 | } else { 77 | // Set the message to the body of the notification as not removing all, we have to have this 78 | notificationContent.body = getNotificationBody(messageContent: messageContent, rootElements: rootElements) 79 | // If we have a value for messageAction passed 80 | if messageContent.messageAction != nil { 81 | // Add messageAction to userInfo 82 | notificationContent.userInfo["messageAction"] = getNotificationBodyAction(messageContent: messageContent, 83 | rootElements: rootElements) 84 | } 85 | // messageButton needs defining, even when not called. So processing it here along with messageButtonAction 86 | let (tempMessageButtonAction, tempCategory) = processMessageButton(notificationCenter: notificationCenter, 87 | messageContent: messageContent, rootElements: rootElements) 88 | // Set the notifications category 89 | notificationCenter.setNotificationCategories([tempCategory]) 90 | // If tempMessageButtonAction has a value 91 | if !tempMessageButtonAction.isEmpty { 92 | // Add messageButtonAction to userInfo 93 | notificationContent.userInfo["messageButtonAction"] = tempMessageButtonAction 94 | } 95 | // If we have a value for messageSound passed 96 | if messageContent.messageSound != nil { 97 | // Set the notifications sound 98 | notificationContent.sound = getNotificationSound(messageContent: messageContent, rootElements: rootElements) 99 | } 100 | // If we've been passed a messageSubtitle 101 | if messageContent.messageSubtitle != nil { 102 | // Set the notifications subtitle 103 | notificationContent.subtitle = getNotificationSubtitle(messageContent: messageContent, 104 | rootElements: rootElements) 105 | } 106 | // If we have a value for messageTitle 107 | if messageContent.messageTitle != nil { 108 | // Set the notifications title 109 | notificationContent.title = getNotificationTitle(messageContent: messageContent, rootElements: rootElements) 110 | } 111 | // If we're to remove a prior posted notification 112 | if rootElements.removeOption == "prior" { 113 | // Remove a specific prior posted notification 114 | removePriorNotification(notificationCenter: notificationCenter, messageContent: messageContent, 115 | passedBase64: passedBase64, rootElements: rootElements) 116 | } else { 117 | // Post the notification 118 | postNotification(notificationCenter: notificationCenter, notificationContent: notificationContent, 119 | messageContent: messageContent, passedBase64: passedBase64, rootElements: rootElements) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Notifier - 16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Notifier - 32x32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Notifier - 32x32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Notifier - 64x64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Notifier - 128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Notifier - 256x256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Notifier - 256x256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Notifier - 512x512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Notifier - 512x512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Notifier - 1024x1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Alerts/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Functions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Functions.swift 3 | // Notifier - Alerts 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import Cocoa 10 | 11 | // Decodes base64String from base64, exiting if fails 12 | func base64Decode(base64String: String) -> String { 13 | // Create a data object from the string, and decode to string 14 | guard let base64EncodedData = base64String.data(using: .utf8), 15 | let encodedData = Data(base64Encoded: base64EncodedData), 16 | let decodedString = String(data: encodedData, encoding: .utf8) 17 | else { 18 | // Post error 19 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Failed to decode: \(base64String) from base64...", 20 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 21 | // Exit 22 | exit(1) 23 | } 24 | // Return's a string decoded from base64 25 | return decodedString 26 | } 27 | 28 | // Decodes the passed JSON 29 | func decodeJSON(passedJSON: String) -> (MessageContent, String, RootElements) { 30 | // Var declaration 31 | var messageContent = MessageContent() 32 | var passedBase64 = String() 33 | var rootElements = RootElements() 34 | // Try to convert RootElements to JSON from base64 35 | do { 36 | // Decode from base64, exiting if fails 37 | let tempDecoded = base64Decode(base64String: passedJSON) 38 | // Turn parsedArguments into JSON 39 | rootElements = try JSONDecoder().decode(RootElements.self, from: Data(tempDecoded.utf8)) 40 | // If verbose mode is set 41 | if rootElements.verboseMode != nil { 42 | // Progress log 43 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(rootElements)") 44 | } 45 | // If encoding into JSON fails 46 | } catch { 47 | // Post error 48 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Failed to decode: \(rootElements) from JSON...", 49 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 50 | // Exit 51 | exit(1) 52 | } 53 | // If we have messageContent 54 | if rootElements.messageContent != nil { 55 | // Try to convert RootElements to JSON from base64 56 | do { 57 | // Set to the base64 passed to the app 58 | passedBase64 = rootElements.messageContent ?? "" 59 | // Decode from base64, exiting if fails 60 | let tempDecoded = base64Decode(base64String: passedBase64) 61 | // Turn messageContent into JSON 62 | messageContent = try JSONDecoder().decode(MessageContent.self, from: Data(tempDecoded.utf8)) 63 | // If verbose mode is set 64 | if rootElements.verboseMode != nil { 65 | // Progress log 66 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(messageContent)") 67 | } 68 | // If encoding into JSON fails 69 | } catch { 70 | // Post error 71 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Failed to decode: \(messageContent) from JSON...", 72 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 73 | // Exit 74 | exit(1) 75 | } 76 | } 77 | // Return messageContent and parsedArguments 78 | return (messageContent, passedBase64, rootElements) 79 | } 80 | 81 | // Logout user, prompting to save 82 | func gracefulLogout(userInfo: [AnyHashable: Any]) { 83 | // Var declaration 84 | var error: NSDictionary? 85 | // If verbose mode is set 86 | if userInfo["verboseMode"] != nil { 87 | // Progress log 88 | NSLog("\(#function.components(separatedBy: "(")[0]) - logout prompting") 89 | } 90 | // Create an NSAppleScript object with the logout command 91 | if let scriptObject = NSAppleScript(source: "tell application \"loginwindow\" to «event aevtlogo»") { 92 | // If we receive output from the prior command 93 | if let outputString = scriptObject.executeAndReturnError(&error).stringValue { 94 | // If verbose mode is set 95 | if userInfo["verboseMode"] != nil { 96 | // Progress log 97 | NSLog("\(#function.components(separatedBy: "(")[0]) - logout - \(outputString)") 98 | } 99 | // If we have an error from the prior command 100 | } else if error != nil { 101 | // Post error 102 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "\(error!)", 103 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 104 | // Exit 105 | exit(1) 106 | } 107 | } 108 | } 109 | 110 | // Checks that notification center is running, and exit if it's not 111 | func isNotificationCenterRunning(verboseMode: String) { 112 | // Exit if Notification Center isn't running for the user 113 | guard !NSRunningApplication.runningApplications(withBundleIdentifier: 114 | "com.apple.notificationcenterui").isEmpty else { 115 | // Post warning 116 | postToNSLogAndStdOut(logLevel: "WARNING", logMessage: "Notification Center is not running...", 117 | functionName: #function.components(separatedBy: "(")[0], verboseMode: verboseMode) 118 | // Exit 119 | exit(0) 120 | } 121 | // If verbose mode is enabled 122 | if verboseMode != "" { 123 | // Progress log 124 | NSLog("\(#function.components(separatedBy: "(")[0]) - Notification Center is running...") 125 | } 126 | } 127 | 128 | // Post to both NSLog and stdout 129 | func postToNSLogAndStdOut(logLevel: String, logMessage: String, functionName: String, verboseMode: String) { 130 | // If verbose mode is enabled 131 | if verboseMode != "" { 132 | // Progress log 133 | NSLog("\(logLevel): \(functionName) - \(logMessage)") 134 | // verbose mode isn't enabled 135 | } else { 136 | // Print to stdout 137 | print("\(logLevel): \(logMessage)") 138 | } 139 | } 140 | 141 | // Runs the passed task 142 | func runTask(taskPath: String, taskArguments: [String], userInfo: [AnyHashable: Any]) -> (String, Bool) { 143 | // Var declaration 144 | var successfulExit = false 145 | // If verbose mode is enabled 146 | if userInfo["verboseMode"] != nil { 147 | // Progress log 148 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)") 149 | } 150 | // Create Task 151 | let task = Process() 152 | // Set task to call the binary 153 | task.executableURL = URL(fileURLWithPath: taskPath) 154 | // Set task arguments 155 | task.arguments = taskArguments 156 | // Create pipe 157 | let outPipe = Pipe() 158 | // Set pipe to be used for stdout 159 | task.standardOutput = outPipe 160 | // Set pipe to be used for stderr 161 | task.standardError = outPipe 162 | // Run the task 163 | try? task.run() 164 | // Wait until task exits 165 | task.waitUntilExit() 166 | // Get output 167 | let outdata = outPipe.fileHandleForReading.readDataToEndOfFile() 168 | // Convert to string 169 | let cmdOut = String(data: outdata, encoding: String.Encoding.utf8) ?? "" 170 | // Return boolean 171 | if task.terminationStatus == 0 { 172 | // Return true 173 | successfulExit = true 174 | // If tasks exits with anything other than 0 175 | } else { 176 | // Return false 177 | successfulExit = false 178 | } 179 | // Return cmdOut text and if the task exited successfully or not 180 | return (cmdOut, successfulExit) 181 | } 182 | -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Notifier - Alerts.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/Structures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Structures.swift 3 | // Notifier - Alerts 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Structure of MessageContent 9 | struct MessageContent: Codable { 10 | // Optional - action to perform when the message is clicked 11 | var messageAction: [TaskObject]? 12 | // The notifications message (required) 13 | var messageBody: String? 14 | // Optional - alert only - message button label 15 | var messageButton: String? 16 | // Optional - alert only - action to perform when the message button is clicked 17 | var messageButtonAction: [TaskObject]? 18 | // Optional - the sound played when the notification has been delivered 19 | var messageSound: String? 20 | // Optional - the notifications subtitle 21 | var messageSubtitle: String? 22 | // Optional - the notifications title 23 | var messageTitle: String? 24 | // Arguments for the task object 25 | struct TaskObject: Codable { 26 | // The tasks executable 27 | var taskPath: String? 28 | // Arguments to pass to the task executable 29 | var taskArguments: [String]? 30 | } 31 | // Initialize MessageContent 32 | init(messageAction: [TaskObject]? = nil, messageBody: String? = nil, messageButton: String? = nil, 33 | messageButtonAction: [TaskObject]? = nil, messageSound: String? = nil, messageSubtitle: String? = nil, 34 | messageTitle: String? = nil) { 35 | self.messageAction = messageAction 36 | self.messageBody = messageBody 37 | self.messageButton = messageButton 38 | self.messageButtonAction = messageButtonAction 39 | self.messageSound = messageSound 40 | self.messageSubtitle = messageSubtitle 41 | self.messageTitle = messageTitle 42 | } 43 | } 44 | 45 | // Structure of RootElements 46 | struct RootElements: Codable { 47 | // Optional - the messages content 48 | var messageContent: String? 49 | // Optional - removes a specific notification or all notifications delivered 50 | var removeOption: String? 51 | // Optional - enables verbose logging 52 | var verboseMode: String? 53 | // Initialize MessageContent 54 | init(messageContent: String? = nil, removeOption: String? = nil, verboseMode: String? = nil) { 55 | self.messageContent = messageContent 56 | self.removeOption = removeOption 57 | self.verboseMode = verboseMode 58 | } 59 | } 60 | 61 | // For storing data in the notifications userInfo 62 | struct UserInfo: Codable { 63 | // Optional - action to perform when the message is clicked 64 | var messageAction: [TaskObject]? 65 | // Optional - action to perform when the message button is clicked 66 | var messageButtonAction: [TaskObject]? 67 | // Arguments for the task object 68 | struct TaskObject: Codable { 69 | // The tasks executable 70 | var taskPath: String? 71 | // Arguments to pass to the task executable 72 | var taskArguments: [String]? 73 | } 74 | // Initialize ParsedArguments 75 | init(messageAction: [TaskObject]? = nil, messageButtonAction: [TaskObject]? = nil) { 76 | self.messageAction = messageAction 77 | self.messageButtonAction = messageButtonAction 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Notifier/Notifier - Alerts/UserNotifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserNotifications.swift 3 | // Notifiers - Alerts 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import UserNotifications 10 | 11 | // Returns the notifications body 12 | func getNotificationBody(messageContent: MessageContent, rootElements: RootElements) -> String { 13 | // If verbose mode is enabled 14 | if rootElements.verboseMode != nil { 15 | // Progress log 16 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageBody: \(messageContent.messageBody!)") 17 | } 18 | // Return messageBody, forcing as this is set unless we remove all.. and if we aren we won't get here 19 | return messageContent.messageBody! 20 | } 21 | 22 | // Returns the notifications body's action 23 | func getNotificationBodyAction(messageContent: MessageContent, rootElements: RootElements) -> [AnyHashable: Any] { 24 | // Var declaration 25 | var messageAction = [AnyHashable: Any]() 26 | // Add taskPath from messagAction to messageAction 27 | messageAction["taskPath"] = messageContent.messageAction?[0].taskPath 28 | // Add taskArguments from messageAction 29 | messageAction["taskArguments"] = messageContent.messageAction?[0].taskArguments 30 | // If verbose mode is enabled 31 | if rootElements.verboseMode != nil { 32 | // Progress log 33 | NSLog(""" 34 | \(#function.components(separatedBy: "(")[0]) - messageAction - taskPath: \ 35 | \(messageAction["taskPath"] ?? ""), taskArguments: \(messageAction["taskArguments"] ?? []) 36 | """) 37 | } 38 | // Return messageAction 39 | return messageAction 40 | } 41 | 42 | // Returns the notifications sound 43 | func getNotificationSound(messageContent: MessageContent, rootElements: RootElements) -> UNNotificationSound { 44 | // Var declaration 45 | var tempSound = UNNotificationSound.default 46 | // If we're not using macOS's default sound 47 | if messageContent.messageSound?.lowercased() != "default" { 48 | // Set the notifications sound 49 | tempSound = UNNotificationSound(named: UNNotificationSoundName(rawValue: messageContent.messageSound ?? "")) 50 | } 51 | // If verbose mode is enabled 52 | if rootElements.verboseMode != nil { 53 | // Progress log 54 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageSound - set to: \(tempSound)") 55 | } 56 | // Return tempSound 57 | return tempSound 58 | } 59 | 60 | // Returns the notifications subtitle 61 | func getNotificationSubtitle(messageContent: MessageContent, rootElements: RootElements) -> String { 62 | // If verbose mode is enabled 63 | if rootElements.verboseMode != nil { 64 | // Progress log 65 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageBody: \(messageContent.messageSubtitle ?? "")") 66 | } 67 | // Return messageSubtitle 68 | return messageContent.messageSubtitle ?? "" 69 | } 70 | 71 | // Returns the notifications title 72 | func getNotificationTitle(messageContent: MessageContent, rootElements: RootElements) -> String { 73 | // If verbose mode is enabled 74 | if rootElements.verboseMode != nil { 75 | // Progress log 76 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageBody: \(messageContent.messageTitle ?? "")") 77 | } 78 | // Return messageTitle 79 | return messageContent.messageTitle ?? "" 80 | } 81 | 82 | // Handles when a notification is interacted with 83 | func handleNotification(forResponse response: UNNotificationResponse) { 84 | // Retrieve userInfo from the response object 85 | let userInfo = response.notification.request.content.userInfo 86 | // If verboseMode is set 87 | if userInfo["verboseMode"] != nil { 88 | // Progress log 89 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - interacted: \(userInfo)") 90 | } 91 | // Triggered when the notification message is clicked 92 | if response.actionIdentifier == "com.apple.UNNotificationDefaultActionIdentifier" { 93 | // If verbose mode is set 94 | if userInfo["verboseMode"] != nil { 95 | // Progress log 96 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - clicked") 97 | } 98 | // Performs any actions set when the user clicks the message 99 | processNotificationActions(userInfoKey: "messageAction", userInfo: userInfo) 100 | // If the notification was dismissed 101 | } else if response.actionIdentifier == "com.apple.UNNotificationDismissActionIdentifier" { 102 | // If verbose mode is set 103 | if userInfo["verboseMode"] != nil { 104 | // Progress log 105 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - message was dismissed") 106 | } 107 | // If the messageButton was clicked 108 | } else { 109 | // If verbose mode is set 110 | if userInfo["verboseMode"] != nil { 111 | // Progress log 112 | NSLog(""" 113 | \(#function.components(separatedBy: "(")[0]) - message button - \ 114 | clicked - userInfo \(String(describing: userInfo)) 115 | """) 116 | } 117 | // Performs any actions set when the user clicks the messagebutton 118 | processNotificationActions(userInfoKey: "messageButtonAction", userInfo: userInfo) 119 | } 120 | // If verbose mode is set 121 | if userInfo["verboseMode"] != nil { 122 | // Progress log 123 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - removing notification") 124 | } 125 | // Remove the delivered notification 126 | UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: 127 | [response.notification.request.identifier]) 128 | // If verbose mode is set 129 | if userInfo["verboseMode"] != nil { 130 | // Progress log 131 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - removing notification - done") 132 | } 133 | // Exit 134 | exit(0) 135 | } 136 | 137 | // Adds messageButton (always needed) and messageButtonAction (when defined) 138 | func processMessageButton(notificationCenter: UNUserNotificationCenter, messageContent: MessageContent, 139 | rootElements: RootElements) -> 140 | ([AnyHashable: Any], UNNotificationCategory) { 141 | // Var declaration 142 | var tempCategory = UNNotificationCategory(identifier: "alert", actions: [], intentIdentifiers: [], 143 | options: .customDismissAction) 144 | var messageButtonAction = [AnyHashable: Any]() 145 | // If we have a value for messageButton passed 146 | if messageContent.messageButton != nil { 147 | // Create an action object 148 | let notificationAction = UNNotificationAction(identifier: "messagebutton", 149 | title: messageContent.messageButton ?? "", 150 | options: []) 151 | // Amend tempCategory 152 | tempCategory = UNNotificationCategory(identifier: "alert", actions: [notificationAction], 153 | intentIdentifiers: [], 154 | options: .customDismissAction) 155 | // If verbose mode is enabled 156 | if rootElements.verboseMode != nil { 157 | // Progress log 158 | NSLog("\(#function.components(separatedBy: "(")[0]) - messagebutton processed") 159 | } 160 | // If we have a values for messageButton and messageButtonAction passed 161 | if messageContent.messageButtonAction != nil { 162 | // Add taskPath from messagAction to messageButtonAction 163 | messageButtonAction["taskPath"] = messageContent.messageButtonAction?[0].taskPath 164 | // Add taskArguments from messageButtonAction 165 | messageButtonAction["taskArguments"] = 166 | messageContent.messageButtonAction?[0].taskArguments 167 | // If verbose mode is enabled 168 | if rootElements.verboseMode != nil { 169 | // Progress log 170 | NSLog(""" 171 | \(#function.components(separatedBy: "(")[0]) - messageButtonAction - taskPath: \ 172 | \(messageButtonAction["taskPath"] ?? ""), 173 | taskArguments: \(messageButtonAction["taskArguments"] ?? []) 174 | """) 175 | } 176 | // Return tempCategory and tempUserInfo 177 | return (messageButtonAction, tempCategory) 178 | } 179 | // If we don't have a value for messageButton 180 | } else { 181 | // If verbose mode is enabled 182 | if rootElements.verboseMode != nil { 183 | // Progress log 184 | NSLog("\(#function.components(separatedBy: "(")[0]) - no messagebutton defined") 185 | } 186 | } 187 | // Return empty userInfo for messageButtonAction and tempCategory 188 | return ([:], tempCategory) 189 | } 190 | 191 | // Post the notification 192 | func postNotification(notificationCenter: UNUserNotificationCenter, notificationContent: UNMutableNotificationContent, 193 | messageContent: MessageContent, passedBase64: String, rootElements: RootElements) { 194 | // If we're in verbose mode 195 | if rootElements.verboseMode != nil { 196 | // Progress log 197 | NSLog(""" 198 | \(#function.components(separatedBy: "(")[0]) - notification \ 199 | request - notificationContent - \(notificationContent). 200 | """) 201 | } 202 | // Create the request object 203 | let notificationRequest = UNNotificationRequest(identifier: passedBase64, content: notificationContent, 204 | trigger: nil) 205 | // Post the notification 206 | notificationCenter.add(notificationRequest) 207 | // If we're in verbose mode 208 | if rootElements.verboseMode != nil { 209 | // Progress log 210 | NSLog("\(#function.components(separatedBy: "(")[0]) - notification delivered") 211 | } 212 | // Sleep, so we don't exit before the notification has been delivered 213 | sleep(1) 214 | // Exit 215 | exit(0) 216 | } 217 | 218 | // Process actions when interacted 219 | func processNotificationActions(userInfoKey: String, userInfo: [AnyHashable: Any]) { 220 | // Var declaration 221 | var messageActionDict = [String: Any]() 222 | // If we have a userInfoKey in the notifications userinfo 223 | if userInfo[userInfoKey] != nil { 224 | // If verbose mode is set 225 | if userInfo["verboseMode"] != nil { 226 | // Progress log 227 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(userInfoKey) - \(userInfo[userInfoKey] ?? [])") 228 | } 229 | // Convert userInfo[userInfoKey] to a dict 230 | messageActionDict = userInfo[userInfoKey] as? [String: Any] ?? [:] 231 | // If we have logout as taskPath 232 | if messageActionDict["taskPath"] as? String == "logout" { 233 | // If verbose mode is set 234 | if userInfo["verboseMode"] != nil { 235 | // Progress log 236 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(userInfoKey) - logout") 237 | } 238 | // Prompt to logout 239 | gracefulLogout(userInfo: userInfo) 240 | // If we have an action thaty's not logout 241 | } else { 242 | // If verbose mode is set 243 | if userInfo["verboseMode"] != nil { 244 | // Progress log 245 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(userInfoKey) - \(userInfo[userInfoKey] ?? [])") 246 | } 247 | // Run the task, returning boolean 248 | let (taskOutput, taskStatus) = runTask(taskPath: messageActionDict["taskPath"] as? String ?? "", 249 | taskArguments: messageActionDict["taskArguments"] 250 | as? [String] ?? [], userInfo: userInfo) 251 | // If verbose mode is set 252 | if userInfo["verboseMode"] != nil { 253 | // If the task completed successfully 254 | if taskStatus { 255 | // Progress log 256 | NSLog(""" 257 | "\(messageActionDict["taskPath"] ?? "") \(messageActionDict["taskArguments"] ?? [])" 258 | completed successfully, returned output: \(taskOutput) 259 | """) 260 | // If task failed to run 261 | } else { 262 | // Post error 263 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: 264 | """ 265 | Running: \(messageActionDict["taskPath"] ?? "") 266 | \(messageActionDict["taskArguments"] ?? []) failed with \(taskOutput). 267 | """, functionName: #function.components(separatedBy: "(")[0], 268 | verboseMode: "enabled") 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | // If we're to remove a specific prior posted notification 276 | func removePriorNotification(notificationCenter: UNUserNotificationCenter, messageContent: MessageContent, 277 | passedBase64: String, rootElements: RootElements) { 278 | // If we're in verbose mode 279 | if rootElements.verboseMode != nil { 280 | // Progress log 281 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove prior - passedBase64 - [\(passedBase64)]") 282 | } 283 | // Remove any prior notifications with the same identifier as ncContentbase64 284 | notificationCenter.removeDeliveredNotifications(withIdentifiers: [passedBase64]) 285 | // If we're in verbose mode 286 | if rootElements.verboseMode != nil { 287 | // Progress log 288 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove prior - done") 289 | } 290 | // Sleep, so we don't exit before notification(s) have been removed 291 | sleep(1) 292 | // Exit 293 | exit(0) 294 | } 295 | 296 | // If we're to remove all prior posted notifications 297 | func removeAllPriorNotifications(notificationCenter: UNUserNotificationCenter, messageContent: MessageContent, 298 | rootElements: RootElements) { 299 | // If we're in verbose mode 300 | if rootElements.verboseMode != nil { 301 | // Verbose message 302 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove all") 303 | } 304 | // Remove all delivered notifications 305 | notificationCenter.removeAllDeliveredNotifications() 306 | // If we're in verbose mode 307 | if rootElements.verboseMode != nil { 308 | // Progress log 309 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove all - done") 310 | } 311 | // Sleep, so we don't exit before notification(s) have been removed 312 | sleep(1) 313 | // Exit 314 | exit(0) 315 | } 316 | 317 | // Request authorisation 318 | func requestAuthorisation(verboseMode: String) { 319 | // Check authorization status with the UNUserNotificationCenter object 320 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { (granted, _) in 321 | if !granted { 322 | // Post error 323 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: """ 324 | Authorisation not granted to post notifications, either manually approve \ 325 | notifications for this application or deploy a Notification PPPCP to this Mac, and \ 326 | try posting the message again... 327 | """, functionName: #function.components(separatedBy: "(")[0], 328 | verboseMode: "verboseMode") 329 | // Exit 330 | exit(1) 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notifier - Notifications 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import Cocoa 10 | import UserNotifications 11 | 12 | // Declaration 13 | @NSApplicationMain 14 | // The apps class 15 | class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { 16 | // IBOutlet declaration 17 | @IBOutlet weak var window: NSWindow! 18 | // When we've finished launching 19 | func applicationDidFinishLaunching(_ aNotification: Notification) { 20 | // If .userInfo ios populated, we've baen launched by interation with a prior posted notification 21 | if let response = (aNotification as NSNotification).userInfo?[ 22 | NSApplication.launchUserNotificationUserInfoKey] as? UNNotificationResponse { 23 | // Handle the notification 24 | handleNotification(forResponse: response) 25 | // Exit 26 | exit(0) 27 | } 28 | // Get the args passed to the binary 29 | let passedCLIArguments = Array(CommandLine.arguments) 30 | // If no args passed, exit 31 | if passedCLIArguments.count == 1 { 32 | // Print error 33 | print("ERROR: No arguments passed to binary, exiting...") 34 | // Exit 35 | exit(0) 36 | // If we have args passed 37 | } else { 38 | // Get the passed base64 string at commandline argument at index 1 39 | let (messageContent, passedBase64, rootElements) = decodeJSON(passedJSON: passedCLIArguments[1]) 40 | // Exit if Notification Center isn't running 41 | isNotificationCenterRunning(verboseMode: rootElements.verboseMode ?? "") 42 | // Ask permission 43 | requestAuthorisation(verboseMode: rootElements.verboseMode ?? "") 44 | // Create a notification center object 45 | let notificationCenter = UNUserNotificationCenter.current() 46 | // Set delegate 47 | notificationCenter.delegate = self 48 | // Process the arguments as needed 49 | processArguments(messageContent: messageContent, notificationCenter: notificationCenter, 50 | passedBase64: passedBase64, passedCLIArguments: passedCLIArguments, 51 | rootElements: rootElements) 52 | } 53 | } 54 | } 55 | 56 | // Process the arguments as needed 57 | func processArguments(messageContent: MessageContent, notificationCenter: UNUserNotificationCenter, 58 | passedBase64: String, passedCLIArguments: [String], rootElements: RootElements) { 59 | // Create a notification content object 60 | let notificationContent = UNMutableNotificationContent() 61 | // Add category identifier to notificationContent required anyway so setting here 62 | notificationContent.categoryIdentifier = "banner" 63 | // If verbose mode is set 64 | if rootElements.verboseMode != nil { 65 | // Add verboseMode to userInfo 66 | notificationContent.userInfo["verboseMode"] = "enabled" 67 | // Progress log 68 | NSLog("\(#function.components(separatedBy: "(")[0]) - verbose enabled") 69 | } 70 | // If we're to remove all delivered notifications 71 | if rootElements.removeOption == "all" { 72 | // Remove all notifications 73 | removeAllPriorNotifications(notificationCenter: notificationCenter, messageContent: messageContent, 74 | rootElements: rootElements) 75 | // If we're not removing 76 | } else { 77 | // Set the message to the body of the notification as not removing all, we have to have this 78 | notificationContent.body = getNotificationBody(messageContent: messageContent, rootElements: rootElements) 79 | // If we have a value for messageAction passed 80 | if messageContent.messageAction != nil { 81 | // Add messageAction to userInfo 82 | notificationContent.userInfo["messageAction"] = getNotificationBodyAction(messageContent: messageContent, 83 | rootElements: rootElements) 84 | } 85 | // messageButton needs defining, even when not called. So processing it here along with messageButtonAction 86 | let (tempMessageButtonAction, tempCategory) = processMessageButton(notificationCenter: notificationCenter, 87 | messageContent: messageContent, rootElements: rootElements) 88 | // Set the notifications category 89 | notificationCenter.setNotificationCategories([tempCategory]) 90 | // If tempMessageButtonAction has a value 91 | if !tempMessageButtonAction.isEmpty { 92 | // Add messageButtonAction to userInfo 93 | notificationContent.userInfo["messageButtonAction"] = tempMessageButtonAction 94 | } 95 | // If we have a value for messageSound passed 96 | if messageContent.messageSound != nil { 97 | // Set the notifications sound 98 | notificationContent.sound = getNotificationSound(messageContent: messageContent, rootElements: rootElements) 99 | } 100 | // If we've been passed a messageSubtitle 101 | if messageContent.messageSubtitle != nil { 102 | // Set the notifications subtitle 103 | notificationContent.subtitle = getNotificationSubtitle(messageContent: messageContent, 104 | rootElements: rootElements) 105 | } 106 | // If we have a value for messageTitle 107 | if messageContent.messageTitle != nil { 108 | // Set the notifications title 109 | notificationContent.title = getNotificationTitle(messageContent: messageContent, rootElements: rootElements) 110 | } 111 | // If we're to remove a prior posted notification 112 | if rootElements.removeOption == "prior" { 113 | // Remove a specific prior posted notification 114 | removePriorNotification(notificationCenter: notificationCenter, messageContent: messageContent, 115 | passedBase64: passedBase64, rootElements: rootElements) 116 | } else { 117 | // Post the notification 118 | postNotification(notificationCenter: notificationCenter, notificationContent: notificationContent, 119 | messageContent: messageContent, passedBase64: passedBase64, rootElements: rootElements) 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Notifier - 16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Notifier - 32x32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Notifier - 32x32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Notifier - 64x64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Notifier - 128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Notifier - 256x256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Notifier - 256x256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Notifier - 512x512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Notifier - 512x512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Notifier - 1024x1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier - Notifications/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Functions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Functions.swift 3 | // Notifier - Notifications 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import Cocoa 10 | 11 | // Decodes base64String from base64, exiting if fails 12 | func base64Decode(base64String: String) -> String { 13 | // Create a data object from the string, and decode to string 14 | guard let base64EncodedData = base64String.data(using: .utf8), 15 | let encodedData = Data(base64Encoded: base64EncodedData), 16 | let decodedString = String(data: encodedData, encoding: .utf8) 17 | else { 18 | // Post error 19 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Failed to decode: \(base64String) from base64...", 20 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 21 | // Exit 22 | exit(1) 23 | } 24 | // Return's a string decoded from base64 25 | return decodedString 26 | } 27 | 28 | // Decodes the passed JSON 29 | func decodeJSON(passedJSON: String) -> (MessageContent, String, RootElements) { 30 | // Var declaration 31 | var messageContent = MessageContent() 32 | var passedBase64 = String() 33 | var rootElements = RootElements() 34 | // Try to convert RootElements to JSON from base64 35 | do { 36 | // Decode from base64, exiting if fails 37 | let tempDecoded = base64Decode(base64String: passedJSON) 38 | // Turn parsedArguments into JSON 39 | rootElements = try JSONDecoder().decode(RootElements.self, from: Data(tempDecoded.utf8)) 40 | // If verbose mode is set 41 | if rootElements.verboseMode != nil { 42 | // Progress log 43 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(rootElements)") 44 | } 45 | // If encoding into JSON fails 46 | } catch { 47 | // Post error 48 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Failed to decode: \(rootElements) from JSON...", 49 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 50 | // Exit 51 | exit(1) 52 | } 53 | // If we have messageContent 54 | if rootElements.messageContent != nil { 55 | // Try to convert RootElements to JSON from base64 56 | do { 57 | // Set to the base64 passed to the app 58 | passedBase64 = rootElements.messageContent ?? "" 59 | // Decode from base64, exiting if fails 60 | let tempDecoded = base64Decode(base64String: passedBase64) 61 | // Turn messageContent into JSON 62 | messageContent = try JSONDecoder().decode(MessageContent.self, from: Data(tempDecoded.utf8)) 63 | // If verbose mode is set 64 | if rootElements.verboseMode != nil { 65 | // Progress log 66 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(messageContent)") 67 | } 68 | // If encoding into JSON fails 69 | } catch { 70 | // Post error 71 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Failed to decode: \(messageContent) from JSON...", 72 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 73 | // Exit 74 | exit(1) 75 | } 76 | } 77 | // Return messageContent and parsedArguments 78 | return (messageContent, passedBase64, rootElements) 79 | } 80 | 81 | // Logout user, prompting to save 82 | func gracefulLogout(userInfo: [AnyHashable: Any]) { 83 | // Var declaration 84 | var error: NSDictionary? 85 | // If verbose mode is set 86 | if userInfo["verboseMode"] != nil { 87 | // Progress log 88 | NSLog("\(#function.components(separatedBy: "(")[0]) - logout prompting") 89 | } 90 | // Create an NSAppleScript object with the logout command 91 | if let scriptObject = NSAppleScript(source: "tell application \"loginwindow\" to «event aevtlogo»") { 92 | // If we receive output from the prior command 93 | if let outputString = scriptObject.executeAndReturnError(&error).stringValue { 94 | // If verbose mode is set 95 | if userInfo["verboseMode"] != nil { 96 | // Progress log 97 | NSLog("\(#function.components(separatedBy: "(")[0]) - logout - \(outputString)") 98 | } 99 | // If we have an error from the prior command 100 | } else if error != nil { 101 | // Post error 102 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "\(error!)", 103 | functionName: #function.components(separatedBy: "(")[0], verboseMode: "enabled") 104 | // Exit 105 | exit(1) 106 | } 107 | } 108 | } 109 | 110 | // Checks that notification center is running, and exit if it's not 111 | func isNotificationCenterRunning(verboseMode: String) { 112 | // Exit if Notification Center isn't running for the user 113 | guard !NSRunningApplication.runningApplications(withBundleIdentifier: 114 | "com.apple.notificationcenterui").isEmpty else { 115 | // Post warning 116 | postToNSLogAndStdOut(logLevel: "WARNING", logMessage: "Notification Center is not running...", 117 | functionName: #function.components(separatedBy: "(")[0], verboseMode: verboseMode) 118 | // Exit 119 | exit(0) 120 | } 121 | // If verbose mode is enabled 122 | if verboseMode != "" { 123 | // Progress log 124 | NSLog("\(#function.components(separatedBy: "(")[0]) - Notification Center is running...") 125 | } 126 | } 127 | 128 | // Post to both NSLog and stdout 129 | func postToNSLogAndStdOut(logLevel: String, logMessage: String, functionName: String, verboseMode: String) { 130 | // If verbose mode is enabled 131 | if verboseMode != "" { 132 | // Progress log 133 | NSLog("\(logLevel): \(functionName) - \(logMessage)") 134 | // verbose mode isn't enabled 135 | } else { 136 | // Print to stdout 137 | print("\(logLevel): \(logMessage)") 138 | } 139 | } 140 | 141 | // Runs the passed task 142 | func runTask(taskPath: String, taskArguments: [String], userInfo: [AnyHashable: Any]) -> (String, Bool) { 143 | // Var declaration 144 | var successfulExit = false 145 | // If verbose mode is enabled 146 | if userInfo["verboseMode"] != nil { 147 | // Progress log 148 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)") 149 | } 150 | // Create Task 151 | let task = Process() 152 | // Set task to call the binary 153 | task.executableURL = URL(fileURLWithPath: taskPath) 154 | // Set task arguments 155 | task.arguments = taskArguments 156 | // Create pipe 157 | let outPipe = Pipe() 158 | // Set pipe to be used for stdout 159 | task.standardOutput = outPipe 160 | // Set pipe to be used for stderr 161 | task.standardError = outPipe 162 | // Run the task 163 | try? task.run() 164 | // Wait until task exits 165 | task.waitUntilExit() 166 | // Get output 167 | let outdata = outPipe.fileHandleForReading.readDataToEndOfFile() 168 | // Convert to string 169 | let cmdOut = String(data: outdata, encoding: String.Encoding.utf8) ?? "" 170 | // Return boolean 171 | if task.terminationStatus == 0 { 172 | // Return true 173 | successfulExit = true 174 | // If tasks exits with anything other than 0 175 | } else { 176 | // Return false 177 | successfulExit = false 178 | } 179 | // Return cmdOut text and if the task exited successfully or not 180 | return (cmdOut, successfulExit) 181 | } 182 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Notifier - Notifications.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Notifier___Notifications.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/Structures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Structures.swift 3 | // Notifier - Notifications 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Structure of MessageContent 9 | struct MessageContent: Codable { 10 | // Optional - action to perform when the message is clicked 11 | var messageAction: [TaskObject]? 12 | // The notifications message (required) 13 | var messageBody: String? 14 | // Optional - message button label 15 | var messageButton: String? 16 | // Optional - action to perform when the message button is clicked 17 | var messageButtonAction: [TaskObject]? 18 | // Optional - the sound played when the notification has been delivered 19 | var messageSound: String? 20 | // Optional - the notifications subtitle 21 | var messageSubtitle: String? 22 | // Optional - the notifications title 23 | var messageTitle: String? 24 | // Arguments for the task object 25 | struct TaskObject: Codable { 26 | // The tasks executable 27 | var taskPath: String? 28 | // Arguments to pass to the task executable 29 | var taskArguments: [String]? 30 | } 31 | // Initialize MessageContent 32 | init(messageAction: [TaskObject]? = nil, messageBody: String? = nil, messageButton: String? = nil, 33 | messageButtonAction: [TaskObject]? = nil, messageSound: String? = nil, messageSubtitle: String? = nil, 34 | messageTitle: String? = nil) { 35 | self.messageAction = messageAction 36 | self.messageBody = messageBody 37 | self.messageButton = messageButton 38 | self.messageButtonAction = messageButtonAction 39 | self.messageSound = messageSound 40 | self.messageSubtitle = messageSubtitle 41 | self.messageTitle = messageTitle 42 | } 43 | } 44 | 45 | // Structure of RootElements 46 | struct RootElements: Codable { 47 | // Optional - the messages content 48 | var messageContent: String? 49 | // Optional - removes a specific notification or all notifications delivered 50 | var removeOption: String? 51 | // Optional - enables verbose logging 52 | var verboseMode: String? 53 | // Initialize MessageContent 54 | init(messageContent: String? = nil, removeOption: String? = nil, verboseMode: String? = nil) { 55 | self.messageContent = messageContent 56 | self.removeOption = removeOption 57 | self.verboseMode = verboseMode 58 | } 59 | } 60 | 61 | // For storing data in the notifications userInfo 62 | struct UserInfo: Codable { 63 | // Optional - action to perform when the message is clicked 64 | var messageAction: [TaskObject]? 65 | // Optional - action to perform when the message button is clicked 66 | var messageButtonAction: [TaskObject]? 67 | // Arguments for the task object 68 | struct TaskObject: Codable { 69 | // The tasks executable 70 | var taskPath: String? 71 | // Arguments to pass to the task executable 72 | var taskArguments: [String]? 73 | } 74 | // Initialize ParsedArguments 75 | init(messageAction: [TaskObject]? = nil, messageButtonAction: [TaskObject]? = nil) { 76 | self.messageAction = messageAction 77 | self.messageButtonAction = messageButtonAction 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Notifier/Notifier - Notifications/UserNotifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserNotifications.swift 3 | // Notifiers - Notifications 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import UserNotifications 10 | 11 | // Returns the notifications body 12 | func getNotificationBody(messageContent: MessageContent, rootElements: RootElements) -> String { 13 | // If verbose mode is enabled 14 | if rootElements.verboseMode != nil { 15 | // Progress log 16 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageBody: \(messageContent.messageBody!)") 17 | } 18 | // Return messageBody, forcing as this is set unless we remove all.. and if we aren we won't get here 19 | return messageContent.messageBody! 20 | } 21 | 22 | // Returns the notifications body's action 23 | func getNotificationBodyAction(messageContent: MessageContent, rootElements: RootElements) -> [AnyHashable: Any] { 24 | // Var declaration 25 | var messageAction = [AnyHashable: Any]() 26 | // Add taskPath from messagAction to messageAction 27 | messageAction["taskPath"] = messageContent.messageAction?[0].taskPath 28 | // Add taskArguments from messageAction 29 | messageAction["taskArguments"] = messageContent.messageAction?[0].taskArguments 30 | // If verbose mode is enabled 31 | if rootElements.verboseMode != nil { 32 | // Progress log 33 | NSLog(""" 34 | \(#function.components(separatedBy: "(")[0]) - messageAction - taskPath: \ 35 | \(messageAction["taskPath"] ?? ""), taskArguments: \(messageAction["taskArguments"] ?? []) 36 | """) 37 | } 38 | // Return messageAction 39 | return messageAction 40 | } 41 | 42 | // Returns the notifications sound 43 | func getNotificationSound(messageContent: MessageContent, rootElements: RootElements) -> UNNotificationSound { 44 | // Var declaration 45 | var tempSound = UNNotificationSound.default 46 | // If we're not using macOS's default sound 47 | if messageContent.messageSound?.lowercased() != "default" { 48 | // Set the notifications sound 49 | tempSound = UNNotificationSound(named: UNNotificationSoundName(rawValue: messageContent.messageSound ?? "")) 50 | } 51 | // If verbose mode is enabled 52 | if rootElements.verboseMode != nil { 53 | // Progress log 54 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageSound - set to: \(tempSound)") 55 | } 56 | // Return tempSound 57 | return tempSound 58 | } 59 | 60 | // Returns the notifications subtitle 61 | func getNotificationSubtitle(messageContent: MessageContent, rootElements: RootElements) -> String { 62 | // If verbose mode is enabled 63 | if rootElements.verboseMode != nil { 64 | // Progress log 65 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageBody: \(messageContent.messageSubtitle ?? "")") 66 | } 67 | // Return messageSubtitle 68 | return messageContent.messageSubtitle ?? "" 69 | } 70 | 71 | // Returns the notifications title 72 | func getNotificationTitle(messageContent: MessageContent, rootElements: RootElements) -> String { 73 | // If verbose mode is enabled 74 | if rootElements.verboseMode != nil { 75 | // Progress log 76 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageBody: \(messageContent.messageTitle ?? "")") 77 | } 78 | // Return messageTitle 79 | return messageContent.messageTitle ?? "" 80 | } 81 | 82 | // Handles when a notification is interacted with 83 | func handleNotification(forResponse response: UNNotificationResponse) { 84 | // Retrieve userInfo from the response object 85 | let userInfo = response.notification.request.content.userInfo 86 | // If verboseMode is set 87 | if userInfo["verboseMode"] != nil { 88 | // Progress log 89 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - interacted: \(userInfo)") 90 | } 91 | // Triggered when the notification message is clicked 92 | if response.actionIdentifier == "com.apple.UNNotificationDefaultActionIdentifier" { 93 | // If verbose mode is set 94 | if userInfo["verboseMode"] != nil { 95 | // Progress log 96 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - clicked") 97 | } 98 | // Performs any actions set when the user clicks the message 99 | processNotificationActions(userInfoKey: "messageAction", userInfo: userInfo) 100 | // If the notification was dismissed 101 | } else if response.actionIdentifier == "com.apple.UNNotificationDismissActionIdentifier" { 102 | // If verbose mode is set 103 | if userInfo["verboseMode"] != nil { 104 | // Progress log 105 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - message was dismissed") 106 | } 107 | // If the messageButton was clicked 108 | } else { 109 | // If verbose mode is set 110 | if userInfo["verboseMode"] != nil { 111 | // Progress log 112 | NSLog(""" 113 | \(#function.components(separatedBy: "(")[0]) - message button - \ 114 | clicked - userInfo \(String(describing: userInfo)) 115 | """) 116 | } 117 | // Performs any actions set when the user clicks the messagebutton 118 | processNotificationActions(userInfoKey: "messageButtonAction", userInfo: userInfo) 119 | } 120 | // If verbose mode is set 121 | if userInfo["verboseMode"] != nil { 122 | // Progress log 123 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - removing notification") 124 | } 125 | // Remove the delivered notification 126 | UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: 127 | [response.notification.request.identifier]) 128 | // If verbose mode is set 129 | if userInfo["verboseMode"] != nil { 130 | // Progress log 131 | NSLog("\(#function.components(separatedBy: "(")[0]) - message - removing notification - done") 132 | } 133 | // Exit 134 | exit(0) 135 | } 136 | 137 | // Adds messageButton (always needed) and messageButtonAction (when defined) 138 | func processMessageButton(notificationCenter: UNUserNotificationCenter, messageContent: MessageContent, 139 | rootElements: RootElements) -> 140 | ([AnyHashable: Any], UNNotificationCategory) { 141 | // Var declaration 142 | var tempCategory = UNNotificationCategory(identifier: "banner", actions: [], intentIdentifiers: [], 143 | options: .customDismissAction) 144 | var messageButtonAction = [AnyHashable: Any]() 145 | // If we have a value for messageButton passed 146 | if messageContent.messageButton != nil { 147 | // Create an action object 148 | let notificationAction = UNNotificationAction(identifier: "messagebutton", 149 | title: messageContent.messageButton ?? "", 150 | options: []) 151 | // Amend tempCategory 152 | tempCategory = UNNotificationCategory(identifier: "banner", actions: [notificationAction], 153 | intentIdentifiers: [], 154 | options: .customDismissAction) 155 | // If verbose mode is enabled 156 | if rootElements.verboseMode != nil { 157 | // Progress log 158 | NSLog("\(#function.components(separatedBy: "(")[0]) - messagebutton processed") 159 | } 160 | // If we have a values for messageButton and messageButtonAction passed 161 | if messageContent.messageButtonAction != nil { 162 | // Add taskPath from messagAction to messageButtonAction 163 | messageButtonAction["taskPath"] = messageContent.messageButtonAction?[0].taskPath 164 | // Add taskArguments from messageButtonAction 165 | messageButtonAction["taskArguments"] = 166 | messageContent.messageButtonAction?[0].taskArguments 167 | // If verbose mode is enabled 168 | if rootElements.verboseMode != nil { 169 | // Progress log 170 | NSLog(""" 171 | \(#function.components(separatedBy: "(")[0]) - messageButtonAction - taskPath: \ 172 | \(messageButtonAction["taskPath"] ?? ""), 173 | taskArguments: \(messageButtonAction["taskArguments"] ?? []) 174 | """) 175 | } 176 | // Return tempCategory and tempUserInfo 177 | return (messageButtonAction, tempCategory) 178 | } 179 | // If we don't have a value for messageButton 180 | } else { 181 | // If verbose mode is enabled 182 | if rootElements.verboseMode != nil { 183 | // Progress log 184 | NSLog("\(#function.components(separatedBy: "(")[0]) - no messagebutton defined") 185 | } 186 | } 187 | // Return empty userInfo for messageButtonAction and tempCategory 188 | return ([:], tempCategory) 189 | } 190 | 191 | // Post the notification 192 | func postNotification(notificationCenter: UNUserNotificationCenter, notificationContent: UNMutableNotificationContent, 193 | messageContent: MessageContent, passedBase64: String, rootElements: RootElements) { 194 | // If we're in verbose mode 195 | if rootElements.verboseMode != nil { 196 | // Progress log 197 | NSLog(""" 198 | \(#function.components(separatedBy: "(")[0]) - notification \ 199 | request - notificationContent - \(notificationContent). 200 | """) 201 | } 202 | // Create the request object 203 | let notificationRequest = UNNotificationRequest(identifier: passedBase64, content: notificationContent, 204 | trigger: nil) 205 | // Post the notification 206 | notificationCenter.add(notificationRequest) 207 | // If we're in verbose mode 208 | if rootElements.verboseMode != nil { 209 | // Progress log 210 | NSLog("\(#function.components(separatedBy: "(")[0]) - notification delivered") 211 | } 212 | // Sleep, so we don't exit before the notification has been delivered 213 | sleep(1) 214 | // Exit 215 | exit(0) 216 | } 217 | 218 | // Process actions when interacted 219 | func processNotificationActions(userInfoKey: String, userInfo: [AnyHashable: Any]) { 220 | // Var declaration 221 | var messageActionDict = [String: Any]() 222 | // If we have a userInfoKey in the notifications userinfo 223 | if userInfo[userInfoKey] != nil { 224 | // If verbose mode is set 225 | if userInfo["verboseMode"] != nil { 226 | // Progress log 227 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(userInfoKey) - \(userInfo[userInfoKey] ?? [])") 228 | } 229 | // Convert userInfo[userInfoKey] to a dict 230 | messageActionDict = userInfo[userInfoKey] as? [String: Any] ?? [:] 231 | // If we have logout as taskPath 232 | if messageActionDict["taskPath"] as? String == "logout" { 233 | // If verbose mode is set 234 | if userInfo["verboseMode"] != nil { 235 | // Progress log 236 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(userInfoKey) - logout") 237 | } 238 | // Prompt to logout 239 | gracefulLogout(userInfo: userInfo) 240 | // If we have an action thaty's not logout 241 | } else { 242 | // If verbose mode is set 243 | if userInfo["verboseMode"] != nil { 244 | // Progress log 245 | NSLog("\(#function.components(separatedBy: "(")[0]) - \(userInfoKey) - \(userInfo[userInfoKey] ?? [])") 246 | } 247 | // Run the task, returning boolean 248 | let (taskOutput, taskStatus) = runTask(taskPath: messageActionDict["taskPath"] as? String ?? "", 249 | taskArguments: messageActionDict["taskArguments"] 250 | as? [String] ?? [], userInfo: userInfo) 251 | // If verbose mode is set 252 | if userInfo["verboseMode"] != nil { 253 | // If the task completed successfully 254 | if taskStatus { 255 | // Progress log 256 | NSLog(""" 257 | "\(messageActionDict["taskPath"] ?? "") \(messageActionDict["taskArguments"] ?? [])" 258 | completed successfully, returned output: \(taskOutput) 259 | """) 260 | // If task failed to run 261 | } else { 262 | // Post error 263 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: 264 | """ 265 | Running: \(messageActionDict["taskPath"] ?? "") 266 | \(messageActionDict["taskArguments"] ?? []) failed with \(taskOutput). 267 | """, functionName: #function.components(separatedBy: "(")[0], 268 | verboseMode: "enabled") 269 | } 270 | } 271 | } 272 | } 273 | } 274 | 275 | // If we're to remove a specific prior posted notification 276 | func removePriorNotification(notificationCenter: UNUserNotificationCenter, messageContent: MessageContent, 277 | passedBase64: String, rootElements: RootElements) { 278 | // If we're in verbose mode 279 | if rootElements.verboseMode != nil { 280 | // Progress log 281 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove prior - passedBase64 - [\(passedBase64)]") 282 | } 283 | // Remove any prior notifications with the same identifier as ncContentbase64 284 | notificationCenter.removeDeliveredNotifications(withIdentifiers: [passedBase64]) 285 | // If we're in verbose mode 286 | if rootElements.verboseMode != nil { 287 | // Progress log 288 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove prior - done") 289 | } 290 | // Sleep, so we don't exit before notification(s) have been removed 291 | sleep(1) 292 | // Exit 293 | exit(0) 294 | } 295 | 296 | // If we're to remove all prior posted notifications 297 | func removeAllPriorNotifications(notificationCenter: UNUserNotificationCenter, messageContent: MessageContent, 298 | rootElements: RootElements) { 299 | // If we're in verbose mode 300 | if rootElements.verboseMode != nil { 301 | // Verbose message 302 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove all") 303 | } 304 | // Remove all delivered notifications 305 | notificationCenter.removeAllDeliveredNotifications() 306 | // If we're in verbose mode 307 | if rootElements.verboseMode != nil { 308 | // Progress log 309 | NSLog("\(#function.components(separatedBy: "(")[0]) - remove all - done") 310 | } 311 | // Sleep, so we don't exit before notification(s) have been removed 312 | sleep(1) 313 | // Exit 314 | exit(0) 315 | } 316 | 317 | // Request authorisation 318 | func requestAuthorisation(verboseMode: String) { 319 | // Check authorization status with the UNUserNotificationCenter object 320 | UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { (granted, _) in 321 | if !granted { 322 | // Post error 323 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: """ 324 | Authorisation not granted to post notifications, either manually approve \ 325 | notifications for this application or deploy a Notification PPPCP to this Mac, and \ 326 | try posting the message again... 327 | """, functionName: #function.components(separatedBy: "(")[0], 328 | verboseMode: "verboseMode") 329 | // Exit 330 | exit(1) 331 | } 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /Notifier/Notifier.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 60; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 63B92DE9282AAEDE00B44340 /* ArgParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63B92DE8282AAEDE00B44340 /* ArgParser.swift */; }; 11 | 7C6D157C2270BA2F0092D34E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6D157B2270BA2F0092D34E /* AppDelegate.swift */; }; 12 | 7C6D15812270BA300092D34E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7C6D157F2270BA300092D34E /* MainMenu.xib */; }; 13 | 7C720952227888E10033B2E2 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C720951227888E10033B2E2 /* Functions.swift */; }; 14 | 8421955B2B67E80A0030A7E4 /* Structures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8421955A2B67E80A0030A7E4 /* Structures.swift */; }; 15 | 844D6FAC2B647A040000CCF2 /* Structures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844D6FAB2B647A040000CCF2 /* Structures.swift */; }; 16 | 84524FD52B61BC620046367B /* Notifier - Notifications.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 848FC8942B61BB1F009B4915 /* Notifier - Notifications.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17 | 845D68E12B692A5100C5B467 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 845D68E02B692A5100C5B467 /* ArgumentParser */; }; 18 | 848FC8972B61BB1F009B4915 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848FC8962B61BB1F009B4915 /* AppDelegate.swift */; }; 19 | 848FC89C2B61BB20009B4915 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 848FC89A2B61BB20009B4915 /* MainMenu.xib */; }; 20 | 849D93A92B693243006410D1 /* UserNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D93A82B693243006410D1 /* UserNotifications.swift */; }; 21 | 849D93AD2B693D2C006410D1 /* Structures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849D93AC2B693D2C006410D1 /* Structures.swift */; }; 22 | 84E1A6BB2B61C00400D7F7C9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1A6BA2B61C00400D7F7C9 /* AppDelegate.swift */; }; 23 | 84E1A6BD2B61C00500D7F7C9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84E1A6BC2B61C00500D7F7C9 /* Assets.xcassets */; }; 24 | 84E1A6C02B61C00500D7F7C9 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84E1A6BE2B61C00500D7F7C9 /* MainMenu.xib */; }; 25 | 84E1A6CB2B61C03F00D7F7C9 /* Notifier - Alerts.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 84E1A6B82B61C00400D7F7C9 /* Notifier - Alerts.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 26 | 84E1A6CD2B61C0B900D7F7C9 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1A6CC2B61C0B900D7F7C9 /* Functions.swift */; }; 27 | 84E1A6CF2B61C0BE00D7F7C9 /* UserNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E1A6CE2B61C0BE00D7F7C9 /* UserNotifications.swift */; }; 28 | 84E333822B61BBBB00BE750F /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E333812B61BBBB00BE750F /* Functions.swift */; }; 29 | 84E333842B61BBC200BE750F /* UserNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E333832B61BBC200BE750F /* UserNotifications.swift */; }; 30 | 84FA74982B61C4A6009B4068 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84E1A6D22B61C1B500D7F7C9 /* Assets.xcassets */; }; 31 | 84FA74992B61C4B7009B4068 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 84E1A6D32B61C1BC00D7F7C9 /* Assets.xcassets */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXContainerItemProxy section */ 35 | 84E1A6C92B61C02C00D7F7C9 /* PBXContainerItemProxy */ = { 36 | isa = PBXContainerItemProxy; 37 | containerPortal = 7C6D15702270BA2F0092D34E /* Project object */; 38 | proxyType = 1; 39 | remoteGlobalIDString = 84E1A6B72B61C00400D7F7C9; 40 | remoteInfo = "Notifier - Alerts"; 41 | }; 42 | 84FA749B2B61C9D9009B4068 /* PBXContainerItemProxy */ = { 43 | isa = PBXContainerItemProxy; 44 | containerPortal = 7C6D15702270BA2F0092D34E /* Project object */; 45 | proxyType = 1; 46 | remoteGlobalIDString = 848FC8932B61BB1F009B4915; 47 | remoteInfo = "Notifier - Notifications"; 48 | }; 49 | /* End PBXContainerItemProxy section */ 50 | 51 | /* Begin PBXCopyFilesBuildPhase section */ 52 | 7CAB609623DF4C8B0029E310 /* CopyFiles */ = { 53 | isa = PBXCopyFilesBuildPhase; 54 | buildActionMask = 2147483647; 55 | dstPath = Contents/Helpers; 56 | dstSubfolderSpec = 1; 57 | files = ( 58 | 84524FD52B61BC620046367B /* Notifier - Notifications.app in CopyFiles */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | 7CF89A4023BF9F6000723A3B /* CopyFiles */ = { 63 | isa = PBXCopyFilesBuildPhase; 64 | buildActionMask = 2147483647; 65 | dstPath = Contents/Helpers; 66 | dstSubfolderSpec = 1; 67 | files = ( 68 | 84E1A6CB2B61C03F00D7F7C9 /* Notifier - Alerts.app in CopyFiles */, 69 | ); 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | /* End PBXCopyFilesBuildPhase section */ 73 | 74 | /* Begin PBXFileReference section */ 75 | 63B92DE8282AAEDE00B44340 /* ArgParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArgParser.swift; sourceTree = ""; }; 76 | 7C6D15782270BA2F0092D34E /* Notifier.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Notifier.app; sourceTree = BUILT_PRODUCTS_DIR; }; 77 | 7C6D157B2270BA2F0092D34E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 78 | 7C6D15802270BA300092D34E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 79 | 7C720951227888E10033B2E2 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 80 | 8421955A2B67E80A0030A7E4 /* Structures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Structures.swift; sourceTree = ""; }; 81 | 844D6FAA2B6447790000CCF2 /* Notifier.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Notifier.entitlements; sourceTree = ""; }; 82 | 844D6FAB2B647A040000CCF2 /* Structures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Structures.swift; sourceTree = ""; }; 83 | 848FC8942B61BB1F009B4915 /* Notifier - Notifications.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Notifier - Notifications.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 84 | 848FC8962B61BB1F009B4915 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 85 | 848FC89B2B61BB20009B4915 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 86 | 849D93A82B693243006410D1 /* UserNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotifications.swift; sourceTree = ""; }; 87 | 849D93AC2B693D2C006410D1 /* Structures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Structures.swift; sourceTree = ""; }; 88 | 84D841A42B7142C6005AD541 /* Notifier - Alerts.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notifier - Alerts.entitlements"; sourceTree = ""; }; 89 | 84E1A6B82B61C00400D7F7C9 /* Notifier - Alerts.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Notifier - Alerts.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 90 | 84E1A6BA2B61C00400D7F7C9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 91 | 84E1A6BC2B61C00500D7F7C9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 92 | 84E1A6BF2B61C00500D7F7C9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 93 | 84E1A6CC2B61C0B900D7F7C9 /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 94 | 84E1A6CE2B61C0BE00D7F7C9 /* UserNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotifications.swift; sourceTree = ""; }; 95 | 84E1A6D22B61C1B500D7F7C9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 96 | 84E1A6D32B61C1BC00D7F7C9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97 | 84E333812B61BBBB00BE750F /* Functions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; 98 | 84E333832B61BBC200BE750F /* UserNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotifications.swift; sourceTree = ""; }; 99 | 84E8D6EE2B714CF100EFEF9C /* Notifier - Notifications.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Notifier - Notifications.entitlements"; sourceTree = ""; }; 100 | /* End PBXFileReference section */ 101 | 102 | /* Begin PBXFrameworksBuildPhase section */ 103 | 7C6D15752270BA2F0092D34E /* Frameworks */ = { 104 | isa = PBXFrameworksBuildPhase; 105 | buildActionMask = 2147483647; 106 | files = ( 107 | 845D68E12B692A5100C5B467 /* ArgumentParser in Frameworks */, 108 | ); 109 | runOnlyForDeploymentPostprocessing = 0; 110 | }; 111 | 848FC8912B61BB1F009B4915 /* Frameworks */ = { 112 | isa = PBXFrameworksBuildPhase; 113 | buildActionMask = 2147483647; 114 | files = ( 115 | ); 116 | runOnlyForDeploymentPostprocessing = 0; 117 | }; 118 | 84E1A6B52B61C00400D7F7C9 /* Frameworks */ = { 119 | isa = PBXFrameworksBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | /* End PBXFrameworksBuildPhase section */ 126 | 127 | /* Begin PBXGroup section */ 128 | 7C6D156F2270BA2F0092D34E = { 129 | isa = PBXGroup; 130 | children = ( 131 | 7C6D157A2270BA2F0092D34E /* Notifier */, 132 | 84E1A6B92B61C00400D7F7C9 /* Notifier - Alerts */, 133 | 848FC8952B61BB1F009B4915 /* Notifier - Notifications */, 134 | 7C6D15792270BA2F0092D34E /* Products */, 135 | 7CECAB5C23BE493E00305F10 /* Frameworks */, 136 | ); 137 | sourceTree = ""; 138 | }; 139 | 7C6D15792270BA2F0092D34E /* Products */ = { 140 | isa = PBXGroup; 141 | children = ( 142 | 7C6D15782270BA2F0092D34E /* Notifier.app */, 143 | 848FC8942B61BB1F009B4915 /* Notifier - Notifications.app */, 144 | 84E1A6B82B61C00400D7F7C9 /* Notifier - Alerts.app */, 145 | ); 146 | name = Products; 147 | sourceTree = ""; 148 | }; 149 | 7C6D157A2270BA2F0092D34E /* Notifier */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 7C6D157B2270BA2F0092D34E /* AppDelegate.swift */, 153 | 63B92DE8282AAEDE00B44340 /* ArgParser.swift */, 154 | 7C720951227888E10033B2E2 /* Functions.swift */, 155 | 844D6FAB2B647A040000CCF2 /* Structures.swift */, 156 | 849D93A82B693243006410D1 /* UserNotifications.swift */, 157 | 84E1A6D32B61C1BC00D7F7C9 /* Assets.xcassets */, 158 | 7C6D157F2270BA300092D34E /* MainMenu.xib */, 159 | 844D6FAA2B6447790000CCF2 /* Notifier.entitlements */, 160 | ); 161 | path = Notifier; 162 | sourceTree = ""; 163 | }; 164 | 7CECAB5C23BE493E00305F10 /* Frameworks */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | ); 168 | name = Frameworks; 169 | sourceTree = ""; 170 | }; 171 | 848FC8952B61BB1F009B4915 /* Notifier - Notifications */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | 848FC8962B61BB1F009B4915 /* AppDelegate.swift */, 175 | 84E333812B61BBBB00BE750F /* Functions.swift */, 176 | 849D93AC2B693D2C006410D1 /* Structures.swift */, 177 | 84E333832B61BBC200BE750F /* UserNotifications.swift */, 178 | 84E1A6D22B61C1B500D7F7C9 /* Assets.xcassets */, 179 | 848FC89A2B61BB20009B4915 /* MainMenu.xib */, 180 | 84E8D6EE2B714CF100EFEF9C /* Notifier - Notifications.entitlements */, 181 | ); 182 | path = "Notifier - Notifications"; 183 | sourceTree = ""; 184 | }; 185 | 84E1A6B92B61C00400D7F7C9 /* Notifier - Alerts */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 84E1A6BA2B61C00400D7F7C9 /* AppDelegate.swift */, 189 | 84E1A6CC2B61C0B900D7F7C9 /* Functions.swift */, 190 | 8421955A2B67E80A0030A7E4 /* Structures.swift */, 191 | 84E1A6CE2B61C0BE00D7F7C9 /* UserNotifications.swift */, 192 | 84E1A6BC2B61C00500D7F7C9 /* Assets.xcassets */, 193 | 84E1A6BE2B61C00500D7F7C9 /* MainMenu.xib */, 194 | 84D841A42B7142C6005AD541 /* Notifier - Alerts.entitlements */, 195 | ); 196 | path = "Notifier - Alerts"; 197 | sourceTree = ""; 198 | }; 199 | /* End PBXGroup section */ 200 | 201 | /* Begin PBXNativeTarget section */ 202 | 7C6D15772270BA2F0092D34E /* Notifier */ = { 203 | isa = PBXNativeTarget; 204 | buildConfigurationList = 7C6D15862270BA300092D34E /* Build configuration list for PBXNativeTarget "Notifier" */; 205 | buildPhases = ( 206 | 7C6D15742270BA2F0092D34E /* Sources */, 207 | 7C6D15752270BA2F0092D34E /* Frameworks */, 208 | 7C6D15762270BA2F0092D34E /* Resources */, 209 | 7CF89A4023BF9F6000723A3B /* CopyFiles */, 210 | 7CAB609623DF4C8B0029E310 /* CopyFiles */, 211 | 84CC99F32C21904900396D3F /* Swiftlint */, 212 | ); 213 | buildRules = ( 214 | ); 215 | dependencies = ( 216 | 84FA749C2B61C9D9009B4068 /* PBXTargetDependency */, 217 | 84E1A6CA2B61C02C00D7F7C9 /* PBXTargetDependency */, 218 | ); 219 | name = Notifier; 220 | packageProductDependencies = ( 221 | 845D68E02B692A5100C5B467 /* ArgumentParser */, 222 | ); 223 | productName = oioi; 224 | productReference = 7C6D15782270BA2F0092D34E /* Notifier.app */; 225 | productType = "com.apple.product-type.application"; 226 | }; 227 | 848FC8932B61BB1F009B4915 /* Notifier - Notifications */ = { 228 | isa = PBXNativeTarget; 229 | buildConfigurationList = 848FC89E2B61BB20009B4915 /* Build configuration list for PBXNativeTarget "Notifier - Notifications" */; 230 | buildPhases = ( 231 | 848FC8902B61BB1F009B4915 /* Sources */, 232 | 848FC8912B61BB1F009B4915 /* Frameworks */, 233 | 848FC8922B61BB1F009B4915 /* Resources */, 234 | ); 235 | buildRules = ( 236 | ); 237 | dependencies = ( 238 | ); 239 | name = "Notifier - Notifications"; 240 | packageProductDependencies = ( 241 | ); 242 | productName = "Notifier - Notifications"; 243 | productReference = 848FC8942B61BB1F009B4915 /* Notifier - Notifications.app */; 244 | productType = "com.apple.product-type.application"; 245 | }; 246 | 84E1A6B72B61C00400D7F7C9 /* Notifier - Alerts */ = { 247 | isa = PBXNativeTarget; 248 | buildConfigurationList = 84E1A6C22B61C00500D7F7C9 /* Build configuration list for PBXNativeTarget "Notifier - Alerts" */; 249 | buildPhases = ( 250 | 84E1A6B42B61C00400D7F7C9 /* Sources */, 251 | 84E1A6B52B61C00400D7F7C9 /* Frameworks */, 252 | 84E1A6B62B61C00400D7F7C9 /* Resources */, 253 | ); 254 | buildRules = ( 255 | ); 256 | dependencies = ( 257 | ); 258 | name = "Notifier - Alerts"; 259 | packageProductDependencies = ( 260 | ); 261 | productName = "Notifier - Alerts"; 262 | productReference = 84E1A6B82B61C00400D7F7C9 /* Notifier - Alerts.app */; 263 | productType = "com.apple.product-type.application"; 264 | }; 265 | /* End PBXNativeTarget section */ 266 | 267 | /* Begin PBXProject section */ 268 | 7C6D15702270BA2F0092D34E /* Project object */ = { 269 | isa = PBXProject; 270 | attributes = { 271 | BuildIndependentTargetsInParallel = YES; 272 | LastSwiftUpdateCheck = 1520; 273 | LastUpgradeCheck = 1600; 274 | ORGANIZATIONNAME = "dataJAR Ltd"; 275 | TargetAttributes = { 276 | 7C6D15772270BA2F0092D34E = { 277 | CreatedOnToolsVersion = 10.1; 278 | LastSwiftMigration = 1020; 279 | SystemCapabilities = { 280 | com.apple.HardenedRuntime = { 281 | enabled = 1; 282 | }; 283 | com.apple.Sandbox = { 284 | enabled = 0; 285 | }; 286 | }; 287 | }; 288 | 848FC8932B61BB1F009B4915 = { 289 | CreatedOnToolsVersion = 15.2; 290 | }; 291 | 84E1A6B72B61C00400D7F7C9 = { 292 | CreatedOnToolsVersion = 15.2; 293 | }; 294 | }; 295 | }; 296 | buildConfigurationList = 7C6D15732270BA2F0092D34E /* Build configuration list for PBXProject "Notifier" */; 297 | compatibilityVersion = "Xcode 15.0"; 298 | developmentRegion = en; 299 | hasScannedForEncodings = 0; 300 | knownRegions = ( 301 | en, 302 | Base, 303 | ); 304 | mainGroup = 7C6D156F2270BA2F0092D34E; 305 | packageReferences = ( 306 | 845D68DF2B692A4800C5B467 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, 307 | ); 308 | productRefGroup = 7C6D15792270BA2F0092D34E /* Products */; 309 | projectDirPath = ""; 310 | projectRoot = ""; 311 | targets = ( 312 | 7C6D15772270BA2F0092D34E /* Notifier */, 313 | 848FC8932B61BB1F009B4915 /* Notifier - Notifications */, 314 | 84E1A6B72B61C00400D7F7C9 /* Notifier - Alerts */, 315 | ); 316 | }; 317 | /* End PBXProject section */ 318 | 319 | /* Begin PBXResourcesBuildPhase section */ 320 | 7C6D15762270BA2F0092D34E /* Resources */ = { 321 | isa = PBXResourcesBuildPhase; 322 | buildActionMask = 2147483647; 323 | files = ( 324 | 84FA74992B61C4B7009B4068 /* Assets.xcassets in Resources */, 325 | 7C6D15812270BA300092D34E /* MainMenu.xib in Resources */, 326 | ); 327 | runOnlyForDeploymentPostprocessing = 0; 328 | }; 329 | 848FC8922B61BB1F009B4915 /* Resources */ = { 330 | isa = PBXResourcesBuildPhase; 331 | buildActionMask = 2147483647; 332 | files = ( 333 | 84FA74982B61C4A6009B4068 /* Assets.xcassets in Resources */, 334 | 848FC89C2B61BB20009B4915 /* MainMenu.xib in Resources */, 335 | ); 336 | runOnlyForDeploymentPostprocessing = 0; 337 | }; 338 | 84E1A6B62B61C00400D7F7C9 /* Resources */ = { 339 | isa = PBXResourcesBuildPhase; 340 | buildActionMask = 2147483647; 341 | files = ( 342 | 84E1A6BD2B61C00500D7F7C9 /* Assets.xcassets in Resources */, 343 | 84E1A6C02B61C00500D7F7C9 /* MainMenu.xib in Resources */, 344 | ); 345 | runOnlyForDeploymentPostprocessing = 0; 346 | }; 347 | /* End PBXResourcesBuildPhase section */ 348 | 349 | /* Begin PBXShellScriptBuildPhase section */ 350 | 84CC99F32C21904900396D3F /* Swiftlint */ = { 351 | isa = PBXShellScriptBuildPhase; 352 | alwaysOutOfDate = 1; 353 | buildActionMask = 2147483647; 354 | files = ( 355 | ); 356 | inputFileListPaths = ( 357 | ); 358 | inputPaths = ( 359 | ); 360 | name = Swiftlint; 361 | outputFileListPaths = ( 362 | ); 363 | outputPaths = ( 364 | ); 365 | runOnlyForDeploymentPostprocessing = 0; 366 | shellPath = /bin/sh; 367 | shellScript = "# If the dir /opt/homebrew/bin/ exists\nif [ -d '/opt/homebrew/bin/' ]\nthen\n # Add homebrew dir to PATH\n PATH=\"/opt/homebrew/bin/:${PATH}\"\nfi\n\n# Get path so swiftlint (if installed)\nswiftlintPath=$(which swiftlint)\n\n# If we can find swiftlint\nif [ -n \"${swiftlintPath}\" ]\nthen\n # Run swiftlint\n swiftlint\n# If we can't find swiftlint\nelse\n # Log error\n /bin/echo \"ERROR: SwiftLint not installed, exiting...\"\n # Exit\n exit 1\nfi\n"; 368 | }; 369 | /* End PBXShellScriptBuildPhase section */ 370 | 371 | /* Begin PBXSourcesBuildPhase section */ 372 | 7C6D15742270BA2F0092D34E /* Sources */ = { 373 | isa = PBXSourcesBuildPhase; 374 | buildActionMask = 2147483647; 375 | files = ( 376 | 7C6D157C2270BA2F0092D34E /* AppDelegate.swift in Sources */, 377 | 7C720952227888E10033B2E2 /* Functions.swift in Sources */, 378 | 63B92DE9282AAEDE00B44340 /* ArgParser.swift in Sources */, 379 | 844D6FAC2B647A040000CCF2 /* Structures.swift in Sources */, 380 | 849D93A92B693243006410D1 /* UserNotifications.swift in Sources */, 381 | ); 382 | runOnlyForDeploymentPostprocessing = 0; 383 | }; 384 | 848FC8902B61BB1F009B4915 /* Sources */ = { 385 | isa = PBXSourcesBuildPhase; 386 | buildActionMask = 2147483647; 387 | files = ( 388 | 848FC8972B61BB1F009B4915 /* AppDelegate.swift in Sources */, 389 | 84E333822B61BBBB00BE750F /* Functions.swift in Sources */, 390 | 84E333842B61BBC200BE750F /* UserNotifications.swift in Sources */, 391 | 849D93AD2B693D2C006410D1 /* Structures.swift in Sources */, 392 | ); 393 | runOnlyForDeploymentPostprocessing = 0; 394 | }; 395 | 84E1A6B42B61C00400D7F7C9 /* Sources */ = { 396 | isa = PBXSourcesBuildPhase; 397 | buildActionMask = 2147483647; 398 | files = ( 399 | 84E1A6BB2B61C00400D7F7C9 /* AppDelegate.swift in Sources */, 400 | 84E1A6CD2B61C0B900D7F7C9 /* Functions.swift in Sources */, 401 | 84E1A6CF2B61C0BE00D7F7C9 /* UserNotifications.swift in Sources */, 402 | 8421955B2B67E80A0030A7E4 /* Structures.swift in Sources */, 403 | ); 404 | runOnlyForDeploymentPostprocessing = 0; 405 | }; 406 | /* End PBXSourcesBuildPhase section */ 407 | 408 | /* Begin PBXTargetDependency section */ 409 | 84E1A6CA2B61C02C00D7F7C9 /* PBXTargetDependency */ = { 410 | isa = PBXTargetDependency; 411 | target = 84E1A6B72B61C00400D7F7C9 /* Notifier - Alerts */; 412 | targetProxy = 84E1A6C92B61C02C00D7F7C9 /* PBXContainerItemProxy */; 413 | }; 414 | 84FA749C2B61C9D9009B4068 /* PBXTargetDependency */ = { 415 | isa = PBXTargetDependency; 416 | target = 848FC8932B61BB1F009B4915 /* Notifier - Notifications */; 417 | targetProxy = 84FA749B2B61C9D9009B4068 /* PBXContainerItemProxy */; 418 | }; 419 | /* End PBXTargetDependency section */ 420 | 421 | /* Begin PBXVariantGroup section */ 422 | 7C6D157F2270BA300092D34E /* MainMenu.xib */ = { 423 | isa = PBXVariantGroup; 424 | children = ( 425 | 7C6D15802270BA300092D34E /* Base */, 426 | ); 427 | name = MainMenu.xib; 428 | sourceTree = ""; 429 | }; 430 | 848FC89A2B61BB20009B4915 /* MainMenu.xib */ = { 431 | isa = PBXVariantGroup; 432 | children = ( 433 | 848FC89B2B61BB20009B4915 /* Base */, 434 | ); 435 | name = MainMenu.xib; 436 | sourceTree = ""; 437 | }; 438 | 84E1A6BE2B61C00500D7F7C9 /* MainMenu.xib */ = { 439 | isa = PBXVariantGroup; 440 | children = ( 441 | 84E1A6BF2B61C00500D7F7C9 /* Base */, 442 | ); 443 | name = MainMenu.xib; 444 | sourceTree = ""; 445 | }; 446 | /* End PBXVariantGroup section */ 447 | 448 | /* Begin XCBuildConfiguration section */ 449 | 7C6D15842270BA300092D34E /* Debug */ = { 450 | isa = XCBuildConfiguration; 451 | buildSettings = { 452 | ALWAYS_SEARCH_USER_PATHS = NO; 453 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 454 | CLANG_ANALYZER_NONNULL = YES; 455 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 456 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 457 | CLANG_CXX_LIBRARY = "libc++"; 458 | CLANG_ENABLE_MODULES = YES; 459 | CLANG_ENABLE_OBJC_ARC = YES; 460 | CLANG_ENABLE_OBJC_WEAK = YES; 461 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 462 | CLANG_WARN_BOOL_CONVERSION = YES; 463 | CLANG_WARN_COMMA = YES; 464 | CLANG_WARN_CONSTANT_CONVERSION = YES; 465 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 466 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 467 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 468 | CLANG_WARN_EMPTY_BODY = YES; 469 | CLANG_WARN_ENUM_CONVERSION = YES; 470 | CLANG_WARN_INFINITE_RECURSION = YES; 471 | CLANG_WARN_INT_CONVERSION = YES; 472 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 473 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 474 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 475 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 476 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 477 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 478 | CLANG_WARN_STRICT_PROTOTYPES = YES; 479 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 480 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 481 | CLANG_WARN_UNREACHABLE_CODE = YES; 482 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 483 | CODE_SIGN_IDENTITY = "Apple Development"; 484 | COPY_PHASE_STRIP = NO; 485 | DEAD_CODE_STRIPPING = YES; 486 | DEBUG_INFORMATION_FORMAT = dwarf; 487 | "DEVELOPMENT_TEAM[sdk=*]" = 82K2XFN8L6; 488 | ENABLE_HARDENED_RUNTIME = YES; 489 | ENABLE_STRICT_OBJC_MSGSEND = YES; 490 | ENABLE_TESTABILITY = YES; 491 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 492 | GCC_C_LANGUAGE_STANDARD = gnu11; 493 | GCC_DYNAMIC_NO_PIC = NO; 494 | GCC_NO_COMMON_BLOCKS = YES; 495 | GCC_OPTIMIZATION_LEVEL = 0; 496 | GCC_PREPROCESSOR_DEFINITIONS = ( 497 | "DEBUG=1", 498 | "$(inherited)", 499 | ); 500 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 501 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 502 | GCC_WARN_UNDECLARED_SELECTOR = YES; 503 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 504 | GCC_WARN_UNUSED_FUNCTION = YES; 505 | GCC_WARN_UNUSED_VARIABLE = YES; 506 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 507 | INFOPLIST_KEY_LSUIElement = YES; 508 | MACOSX_DEPLOYMENT_TARGET = 10.15; 509 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 510 | MTL_FAST_MATH = YES; 511 | ONLY_ACTIVE_ARCH = YES; 512 | "OTHER_CODE_SIGN_FLAGS[sdk=*]" = "--timestamp"; 513 | SDKROOT = macosx; 514 | SKIP_INSTALL = YES; 515 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 516 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 517 | SWIFT_VERSION = 5.0; 518 | }; 519 | name = Debug; 520 | }; 521 | 7C6D15852270BA300092D34E /* Release */ = { 522 | isa = XCBuildConfiguration; 523 | buildSettings = { 524 | ALWAYS_SEARCH_USER_PATHS = NO; 525 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 526 | CLANG_ANALYZER_NONNULL = YES; 527 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 528 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 529 | CLANG_CXX_LIBRARY = "libc++"; 530 | CLANG_ENABLE_MODULES = YES; 531 | CLANG_ENABLE_OBJC_ARC = YES; 532 | CLANG_ENABLE_OBJC_WEAK = YES; 533 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 534 | CLANG_WARN_BOOL_CONVERSION = YES; 535 | CLANG_WARN_COMMA = YES; 536 | CLANG_WARN_CONSTANT_CONVERSION = YES; 537 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 538 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 539 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 540 | CLANG_WARN_EMPTY_BODY = YES; 541 | CLANG_WARN_ENUM_CONVERSION = YES; 542 | CLANG_WARN_INFINITE_RECURSION = YES; 543 | CLANG_WARN_INT_CONVERSION = YES; 544 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 545 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 546 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 547 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 548 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 549 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 550 | CLANG_WARN_STRICT_PROTOTYPES = YES; 551 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 552 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 553 | CLANG_WARN_UNREACHABLE_CODE = YES; 554 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 555 | CODE_SIGN_IDENTITY = "Apple Development"; 556 | COPY_PHASE_STRIP = NO; 557 | DEAD_CODE_STRIPPING = YES; 558 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 559 | DEVELOPMENT_TEAM = 82K2XFN8L6; 560 | ENABLE_HARDENED_RUNTIME = YES; 561 | ENABLE_NS_ASSERTIONS = NO; 562 | ENABLE_STRICT_OBJC_MSGSEND = YES; 563 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 564 | GCC_C_LANGUAGE_STANDARD = gnu11; 565 | GCC_NO_COMMON_BLOCKS = YES; 566 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 567 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 568 | GCC_WARN_UNDECLARED_SELECTOR = YES; 569 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 570 | GCC_WARN_UNUSED_FUNCTION = YES; 571 | GCC_WARN_UNUSED_VARIABLE = YES; 572 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 573 | INFOPLIST_KEY_LSUIElement = YES; 574 | MACOSX_DEPLOYMENT_TARGET = 10.15; 575 | MTL_ENABLE_DEBUG_INFO = NO; 576 | MTL_FAST_MATH = YES; 577 | "OTHER_CODE_SIGN_FLAGS[sdk=*]" = "--timestamp"; 578 | SDKROOT = macosx; 579 | SKIP_INSTALL = YES; 580 | SWIFT_COMPILATION_MODE = wholemodule; 581 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 582 | SWIFT_VERSION = 5.0; 583 | }; 584 | name = Release; 585 | }; 586 | 7C6D15872270BA300092D34E /* Debug */ = { 587 | isa = XCBuildConfiguration; 588 | buildSettings = { 589 | ARCHS = "$(ARCHS_STANDARD)"; 590 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 591 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; 592 | CODE_SIGN_ENTITLEMENTS = Notifier/Notifier.entitlements; 593 | CODE_SIGN_IDENTITY = "Apple Development"; 594 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 595 | CODE_SIGN_STYLE = Manual; 596 | COMBINE_HIDPI_IMAGES = YES; 597 | DEAD_CODE_STRIPPING = YES; 598 | DEVELOPMENT_TEAM = ""; 599 | "DEVELOPMENT_TEAM[sdk=macosx*]" = 82K2XFN8L6; 600 | ENABLE_HARDENED_RUNTIME = YES; 601 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 602 | INFOPLIST_FILE = Notifier/Info.plist; 603 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 604 | INFOPLIST_KEY_LSUIElement = YES; 605 | LD_RUNPATH_SEARCH_PATHS = ( 606 | "$(inherited)", 607 | "@executable_path/../Frameworks", 608 | ); 609 | MACOSX_DEPLOYMENT_TARGET = 10.15; 610 | MARKETING_VERSION = 3.1; 611 | ONLY_ACTIVE_ARCH = YES; 612 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.dataJAR.Notifier; 613 | PRODUCT_NAME = "$(TARGET_NAME)"; 614 | PROVISIONING_PROFILE_SPECIFIER = ""; 615 | SKIP_INSTALL = NO; 616 | SWIFT_INCLUDE_PATHS = ""; 617 | SWIFT_VERSION = 5.0; 618 | }; 619 | name = Debug; 620 | }; 621 | 7C6D15882270BA300092D34E /* Release */ = { 622 | isa = XCBuildConfiguration; 623 | buildSettings = { 624 | ARCHS = "$(ARCHS_STANDARD)"; 625 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 626 | ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; 627 | CODE_SIGN_ENTITLEMENTS = Notifier/Notifier.entitlements; 628 | CODE_SIGN_IDENTITY = "Apple Development"; 629 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 630 | CODE_SIGN_STYLE = Manual; 631 | COMBINE_HIDPI_IMAGES = YES; 632 | DEAD_CODE_STRIPPING = YES; 633 | DEVELOPMENT_TEAM = ""; 634 | "DEVELOPMENT_TEAM[sdk=macosx*]" = 82K2XFN8L6; 635 | ENABLE_HARDENED_RUNTIME = YES; 636 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 637 | INFOPLIST_FILE = Notifier/Info.plist; 638 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 639 | INFOPLIST_KEY_LSUIElement = YES; 640 | LD_RUNPATH_SEARCH_PATHS = ( 641 | "$(inherited)", 642 | "@executable_path/../Frameworks", 643 | ); 644 | MACOSX_DEPLOYMENT_TARGET = 10.15; 645 | MARKETING_VERSION = 3.1; 646 | ONLY_ACTIVE_ARCH = NO; 647 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.dataJAR.Notifier; 648 | PRODUCT_NAME = "$(TARGET_NAME)"; 649 | PROVISIONING_PROFILE_SPECIFIER = ""; 650 | SKIP_INSTALL = NO; 651 | SWIFT_INCLUDE_PATHS = ""; 652 | SWIFT_VERSION = 5.0; 653 | }; 654 | name = Release; 655 | }; 656 | 848FC89F2B61BB20009B4915 /* Debug */ = { 657 | isa = XCBuildConfiguration; 658 | buildSettings = { 659 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 660 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 661 | CODE_SIGN_ENTITLEMENTS = "Notifier - Notifications/Notifier - Notifications.entitlements"; 662 | CODE_SIGN_IDENTITY = "Apple Development"; 663 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 664 | CODE_SIGN_STYLE = Manual; 665 | COMBINE_HIDPI_IMAGES = YES; 666 | DEVELOPMENT_TEAM = ""; 667 | "DEVELOPMENT_TEAM[sdk=macosx*]" = 82K2XFN8L6; 668 | ENABLE_HARDENED_RUNTIME = YES; 669 | GCC_C_LANGUAGE_STANDARD = gnu17; 670 | GENERATE_INFOPLIST_FILE = YES; 671 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 672 | INFOPLIST_KEY_LSUIElement = YES; 673 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 dataJAR Ltd. All rights reserved."; 674 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 675 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 676 | LD_RUNPATH_SEARCH_PATHS = ( 677 | "$(inherited)", 678 | "@executable_path/../Frameworks", 679 | ); 680 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 681 | MACOSX_DEPLOYMENT_TARGET = 10.15; 682 | MARKETING_VERSION = 3.1; 683 | NEW_SETTING = ""; 684 | ONLY_ACTIVE_ARCH = YES; 685 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.dataJAR.NotifierNotifications; 686 | PRODUCT_NAME = "$(TARGET_NAME)"; 687 | PROVISIONING_PROFILE_SPECIFIER = ""; 688 | SKIP_INSTALL = YES; 689 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 690 | SWIFT_EMIT_LOC_STRINGS = YES; 691 | SWIFT_VERSION = 5.0; 692 | }; 693 | name = Debug; 694 | }; 695 | 848FC8A02B61BB20009B4915 /* Release */ = { 696 | isa = XCBuildConfiguration; 697 | buildSettings = { 698 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 699 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 700 | CODE_SIGN_ENTITLEMENTS = "Notifier - Notifications/Notifier - Notifications.entitlements"; 701 | CODE_SIGN_IDENTITY = "Apple Development"; 702 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 703 | CODE_SIGN_STYLE = Manual; 704 | COMBINE_HIDPI_IMAGES = YES; 705 | DEVELOPMENT_TEAM = ""; 706 | "DEVELOPMENT_TEAM[sdk=macosx*]" = 82K2XFN8L6; 707 | ENABLE_HARDENED_RUNTIME = YES; 708 | GCC_C_LANGUAGE_STANDARD = gnu17; 709 | GENERATE_INFOPLIST_FILE = YES; 710 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 711 | INFOPLIST_KEY_LSUIElement = YES; 712 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 dataJAR Ltd. All rights reserved."; 713 | INFOPLIST_KEY_NSMainNibFile = MainMenu; 714 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 715 | LD_RUNPATH_SEARCH_PATHS = ( 716 | "$(inherited)", 717 | "@executable_path/../Frameworks", 718 | ); 719 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 720 | MACOSX_DEPLOYMENT_TARGET = 10.15; 721 | MARKETING_VERSION = 3.1; 722 | NEW_SETTING = ""; 723 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.dataJAR.NotifierNotifications; 724 | PRODUCT_NAME = "$(TARGET_NAME)"; 725 | PROVISIONING_PROFILE_SPECIFIER = ""; 726 | SKIP_INSTALL = YES; 727 | SWIFT_EMIT_LOC_STRINGS = YES; 728 | SWIFT_VERSION = 5.0; 729 | }; 730 | name = Release; 731 | }; 732 | 84E1A6C32B61C00500D7F7C9 /* Debug */ = { 733 | isa = XCBuildConfiguration; 734 | buildSettings = { 735 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 736 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 737 | CODE_SIGN_ENTITLEMENTS = "Notifier - Alerts/Notifier - Alerts.entitlements"; 738 | CODE_SIGN_IDENTITY = "Apple Development"; 739 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 740 | CODE_SIGN_STYLE = Manual; 741 | COMBINE_HIDPI_IMAGES = YES; 742 | DEVELOPMENT_TEAM = ""; 743 | "DEVELOPMENT_TEAM[sdk=macosx*]" = 82K2XFN8L6; 744 | ENABLE_HARDENED_RUNTIME = YES; 745 | GCC_C_LANGUAGE_STANDARD = gnu17; 746 | GENERATE_INFOPLIST_FILE = YES; 747 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 748 | INFOPLIST_KEY_LSUIElement = YES; 749 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 dataJAR Ltd. All rights reserved."; 750 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 751 | LD_RUNPATH_SEARCH_PATHS = ( 752 | "$(inherited)", 753 | "@executable_path/../Frameworks", 754 | ); 755 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 756 | MACOSX_DEPLOYMENT_TARGET = 10.15; 757 | MARKETING_VERSION = 3.1; 758 | ONLY_ACTIVE_ARCH = YES; 759 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.dataJAR.NotifierAlerts; 760 | PRODUCT_NAME = "$(TARGET_NAME)"; 761 | PROVISIONING_PROFILE_SPECIFIER = ""; 762 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 763 | SWIFT_EMIT_LOC_STRINGS = YES; 764 | SWIFT_VERSION = 5.0; 765 | }; 766 | name = Debug; 767 | }; 768 | 84E1A6C42B61C00500D7F7C9 /* Release */ = { 769 | isa = XCBuildConfiguration; 770 | buildSettings = { 771 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 772 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 773 | CODE_SIGN_ENTITLEMENTS = "Notifier - Alerts/Notifier - Alerts.entitlements"; 774 | CODE_SIGN_IDENTITY = "Apple Development"; 775 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; 776 | CODE_SIGN_STYLE = Manual; 777 | COMBINE_HIDPI_IMAGES = YES; 778 | DEVELOPMENT_TEAM = ""; 779 | "DEVELOPMENT_TEAM[sdk=macosx*]" = 82K2XFN8L6; 780 | ENABLE_HARDENED_RUNTIME = YES; 781 | GCC_C_LANGUAGE_STANDARD = gnu17; 782 | GENERATE_INFOPLIST_FILE = YES; 783 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 784 | INFOPLIST_KEY_LSUIElement = YES; 785 | INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 dataJAR Ltd. All rights reserved."; 786 | INFOPLIST_KEY_NSPrincipalClass = NSApplication; 787 | LD_RUNPATH_SEARCH_PATHS = ( 788 | "$(inherited)", 789 | "@executable_path/../Frameworks", 790 | ); 791 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 792 | MACOSX_DEPLOYMENT_TARGET = 10.15; 793 | MARKETING_VERSION = 3.1; 794 | PRODUCT_BUNDLE_IDENTIFIER = uk.co.dataJAR.NotifierAlerts; 795 | PRODUCT_NAME = "$(TARGET_NAME)"; 796 | PROVISIONING_PROFILE_SPECIFIER = ""; 797 | SWIFT_EMIT_LOC_STRINGS = YES; 798 | SWIFT_VERSION = 5.0; 799 | }; 800 | name = Release; 801 | }; 802 | /* End XCBuildConfiguration section */ 803 | 804 | /* Begin XCConfigurationList section */ 805 | 7C6D15732270BA2F0092D34E /* Build configuration list for PBXProject "Notifier" */ = { 806 | isa = XCConfigurationList; 807 | buildConfigurations = ( 808 | 7C6D15842270BA300092D34E /* Debug */, 809 | 7C6D15852270BA300092D34E /* Release */, 810 | ); 811 | defaultConfigurationIsVisible = 0; 812 | defaultConfigurationName = Release; 813 | }; 814 | 7C6D15862270BA300092D34E /* Build configuration list for PBXNativeTarget "Notifier" */ = { 815 | isa = XCConfigurationList; 816 | buildConfigurations = ( 817 | 7C6D15872270BA300092D34E /* Debug */, 818 | 7C6D15882270BA300092D34E /* Release */, 819 | ); 820 | defaultConfigurationIsVisible = 0; 821 | defaultConfigurationName = Release; 822 | }; 823 | 848FC89E2B61BB20009B4915 /* Build configuration list for PBXNativeTarget "Notifier - Notifications" */ = { 824 | isa = XCConfigurationList; 825 | buildConfigurations = ( 826 | 848FC89F2B61BB20009B4915 /* Debug */, 827 | 848FC8A02B61BB20009B4915 /* Release */, 828 | ); 829 | defaultConfigurationIsVisible = 0; 830 | defaultConfigurationName = Release; 831 | }; 832 | 84E1A6C22B61C00500D7F7C9 /* Build configuration list for PBXNativeTarget "Notifier - Alerts" */ = { 833 | isa = XCConfigurationList; 834 | buildConfigurations = ( 835 | 84E1A6C32B61C00500D7F7C9 /* Debug */, 836 | 84E1A6C42B61C00500D7F7C9 /* Release */, 837 | ); 838 | defaultConfigurationIsVisible = 0; 839 | defaultConfigurationName = Release; 840 | }; 841 | /* End XCConfigurationList section */ 842 | 843 | /* Begin XCRemoteSwiftPackageReference section */ 844 | 845D68DF2B692A4800C5B467 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { 845 | isa = XCRemoteSwiftPackageReference; 846 | repositoryURL = "https://github.com/apple/swift-argument-parser.git"; 847 | requirement = { 848 | kind = upToNextMajorVersion; 849 | minimumVersion = 1.2.3; 850 | }; 851 | }; 852 | /* End XCRemoteSwiftPackageReference section */ 853 | 854 | /* Begin XCSwiftPackageProductDependency section */ 855 | 845D68E02B692A5100C5B467 /* ArgumentParser */ = { 856 | isa = XCSwiftPackageProductDependency; 857 | package = 845D68DF2B692A4800C5B467 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; 858 | productName = ArgumentParser; 859 | }; 860 | /* End XCSwiftPackageProductDependency section */ 861 | }; 862 | rootObject = 7C6D15702270BA2F0092D34E /* Project object */; 863 | } 864 | -------------------------------------------------------------------------------- /Notifier/Notifier.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Notifier/Notifier.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Notifier/Notifier.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "59ba1edda695b389d6c9ac1809891cd779e4024f505b0ce1a9d5202b6762e38a", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-argument-parser", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-argument-parser.git", 8 | "state" : { 9 | "revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531", 10 | "version" : "1.2.3" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Notifier/Notifier.xcodeproj/xcshareddata/xcschemes/Notifier.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Notifier/Notifier/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Notifier 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import Cocoa 10 | import CoreFoundation 11 | import UserNotifications 12 | 13 | // Declaration 14 | @NSApplicationMain 15 | // The apps class 16 | class AppDelegate: NSObject, NSApplicationDelegate { 17 | // IBOutlet declaration 18 | @IBOutlet weak var window: NSWindow! 19 | // If we're about to load, make sure we're logged in, exit otherwise 20 | func applicationWillFinishLaunching(_ aNotification: Notification) { 21 | // Get the username of the logged in user 22 | let loggedInUser = loggedInUser() 23 | // The first argument is always the executable, drop it 24 | let passedArgs = Array(CommandLine.arguments.dropFirst()) 25 | // Get the parsed args 26 | let parsedResult = ArgParser.parseOrExit(passedArgs) 27 | // If rebrand has been passed 28 | if parsedResult.rebrand != "" { 29 | // Rebrand Notifier apps 30 | changeIcons(brandingImage: parsedResult.rebrand, loggedInUser: loggedInUser, parsedResult: parsedResult) 31 | // If we're not rebranding and no user is logged in, exit 32 | } else if loggedInUser == "" { 33 | // Post message 34 | print("INFO: No user logged in, and we're not rebranding... exiting...") 35 | // Exit 36 | exit(0) 37 | } 38 | } 39 | // Check arguments passed to app, then OS if a valid argument is passed else print usage 40 | func applicationDidFinishLaunching(_ aNotification: Notification) { 41 | // The first argument is always the executable, drop it 42 | let passedArgs = Array(CommandLine.arguments.dropFirst()) 43 | // Get the parsed args 44 | let parsedResult = ArgParser.parseOrExit(passedArgs) 45 | // Get the username of the logged in user 46 | let loggedInUser = loggedInUser() 47 | // If verbose mode is enabled 48 | if parsedResult.verbose { 49 | // Progress log 50 | NSLog("\(#function.components(separatedBy: "(")[0]) - verbose enabled - arguments: \(parsedResult)") 51 | } 52 | // If rebrand has been passed 53 | if parsedResult.rebrand != "" { 54 | // Rebrand Notifier apps 55 | changeIcons(brandingImage: parsedResult.rebrand, loggedInUser: loggedInUser, parsedResult: parsedResult) 56 | // If we're not rebranding and no user is logged in, exit 57 | } else if loggedInUser == "" { 58 | // Post message 59 | postToNSLogAndStdOut(logLevel: "INFO", logMessage: "No user logged in, exiting...", 60 | functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 61 | // Exit 62 | exit(0) 63 | // If we have no value passed to --type 64 | } else if parsedResult.type == "" { 65 | // Show help 66 | _ = ArgParser.parseOrExit(["--help"]) 67 | // Exit 68 | exit(1) 69 | } 70 | // Exit if Notification Center isn't running for the user 71 | isNotificationCenterRunning(parsedResult: parsedResult) 72 | // Check that we have all the needed arguments and that they are valid options, exiting if not 73 | checkArgs(parsedResult: parsedResult) 74 | // If verbose mode is enabled 75 | if parsedResult.verbose { 76 | // Progress log 77 | NSLog("\(#function.components(separatedBy: "(")[0]) - type - \(parsedResult.type)") 78 | } 79 | // Var declaration 80 | var notifierPath = String() 81 | // If we're looking for the alert app 82 | if parsedResult.type == "alert" { 83 | // Get the alert apps path 84 | notifierPath = GlobalVariables.alertAppPath + "/Contents/MacOS/Notifier - Alerts" 85 | // If we're looking for the notifications app 86 | } else { 87 | // Get the alert apps path 88 | notifierPath = GlobalVariables.bannerAppPath + "/Contents/MacOS/Notifier - Notifications" 89 | } 90 | // If --remove all has been passed 91 | if parsedResult.remove.lowercased() == "all" { 92 | // Initialize a rootElements object 93 | var rootElements = RootElements(removeOption: "all") 94 | // Initialize a messageContent object 95 | let messageContent = MessageContent() 96 | // If verbose mode is enabled 97 | if parsedResult.verbose { 98 | // Set verboseMode 99 | rootElements.verboseMode = "enabled" 100 | } 101 | // Create the JSON to pass to the notifying app 102 | let commandJSON = createJSON(messageContent: messageContent, parsedResult: parsedResult, 103 | rootElements: rootElements) 104 | // Pass commandJSON to the relevant app, exiting afterwards 105 | passToApp(commandJSON: commandJSON, loggedInUser: loggedInUser, notifierPath: notifierPath, 106 | parsedResult: parsedResult) 107 | } else { 108 | // Format the args as needed 109 | formatArgs(loggedInUser: loggedInUser, notifierPath: notifierPath, parsedResult: parsedResult) 110 | } 111 | } 112 | } 113 | 114 | // Check that we have all the needed arguments and that they are valid options, exiting if not 115 | func checkArgs(parsedResult: ArgParser) { 116 | // If an invalid value for --type has been passed 117 | if parsedResult.type.lowercased() != "alert" && parsedResult.type.lowercased() != "banner" { 118 | // Post message 119 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "incorrect argument passed to --type, exiting...\n", 120 | functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 121 | // Show help 122 | _ = ArgParser.parseOrExit(["--help"]) 123 | // Exit 124 | exit(1) 125 | } 126 | // If an invalid value for --remove has been passed 127 | if parsedResult.remove != "" && parsedResult.remove.lowercased() != "all" && parsedResult.remove.lowercased() 128 | != "prior" { 129 | // Post message 130 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "incorrect argument passed to --remove, exiting...\n", 131 | functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 132 | // Show help 133 | _ = ArgParser.parseOrExit(["--help"]) 134 | // Exit 135 | exit(1) 136 | } 137 | // If we've got here and no --message has been passed and we're not removing 138 | if parsedResult.remove == "" && parsedResult.message == "" { 139 | // Post message 140 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: """ 141 | missing value for --message and we're neither rebranding nor removing, exiting...\n 142 | """, functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 143 | // Show help 144 | _ = ArgParser.parseOrExit(["--help"]) 145 | // Exit 146 | exit(1) 147 | } 148 | } 149 | 150 | // Format the args as needed 151 | func formatArgs(loggedInUser: String, notifierPath: String, parsedResult: ArgParser) { 152 | // Initialize a messageContent object 153 | var messageContent = MessageContent() 154 | // Initialize a rootElements object 155 | var rootElements = RootElements() 156 | // If verbose mode is enabled 157 | if parsedResult.verbose { 158 | // Set verboseMode 159 | rootElements.verboseMode = "enabled" 160 | } 161 | // Set the message to the body of the notification as not removing all, we have to have this 162 | messageContent.messageBody = setNotificationBody(parsedResult: parsedResult) 163 | // If we've been passed a messageaction 164 | if parsedResult.messageaction != "" { 165 | // Set messageAction 166 | messageContent.messageAction = parseAction(actionString: parsedResult.messageaction, 167 | parsedResult: parsedResult) 168 | } 169 | // If we've been passed a sound 170 | if parsedResult.sound != "" { 171 | // Set messageSound 172 | messageContent.messageSound = setNotificationSound(parsedResult: parsedResult) 173 | } 174 | // If we've been passed a subtitle 175 | if parsedResult.subtitle != "" { 176 | // Set messageSubtitle 177 | messageContent.messageSubtitle = setNotificationSubtitle(parsedResult: parsedResult) 178 | } 179 | // If we've been passed a title 180 | if parsedResult.title != "" { 181 | // Set messageTitle 182 | messageContent.messageTitle = setNotificationTitle(parsedResult: parsedResult) 183 | } 184 | // If we're to remove a prior posted notification 185 | if parsedResult.remove.lowercased() == "prior" { 186 | // Set removeOption 187 | rootElements.removeOption = "prior" 188 | // If verbose mode is enabled 189 | if parsedResult.verbose { 190 | // Progress log 191 | NSLog("\(#function.components(separatedBy: "(")[0]) - removeOption: \(rootElements.removeOption!))") 192 | } 193 | } 194 | // If we've been passed a messagebutton, and messagebuttonaction 195 | if parsedResult.messagebutton != "" { 196 | // Set messageButton and messagebuttonaction 197 | messageContent.messageButton = setNotificationMessageButton(parsedResult: parsedResult) 198 | // If we've been passed a messagebuttonaction, only set if a messagebutton was passed too 199 | if parsedResult.messagebuttonaction != "" { 200 | // Set messageButtonAction 201 | messageContent.messageButtonAction = parseAction(actionString: 202 | parsedResult.messagebuttonaction, 203 | parsedResult: parsedResult) 204 | } 205 | } 206 | // Create the JSON to pass to the notifying app 207 | let commandJSON = createJSON(messageContent: messageContent, parsedResult: parsedResult, 208 | rootElements: rootElements) 209 | // Pass commandJSON to the relevant app, exiting afterwards 210 | passToApp(commandJSON: commandJSON, loggedInUser: loggedInUser, notifierPath: notifierPath, 211 | parsedResult: parsedResult) 212 | } 213 | -------------------------------------------------------------------------------- /Notifier/Notifier/ArgParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArgParser.swift 3 | // Notifier 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import ArgumentParser 10 | import Foundation 11 | 12 | // Struct for ArgParser 13 | struct ArgParser: ParsableCommand { 14 | // Set overview and usage text 15 | static let configuration = CommandConfiguration( 16 | abstract: """ 17 | Notifier \(String(describing: 18 | Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")!)): \ 19 | Posts alert or banner notifications. 20 | """, 21 | usage: """ 22 | --type --message 23 | --type --remove prior 24 | --type --remove all 25 | --rebrand 26 | """, 27 | helpNames: [.long] 28 | ) 29 | // Required - notification type 30 | @Option(help: """ 31 | alert or banner - REQUIRED. 32 | 33 | """) 34 | var type: String = "" 35 | // The notifications message 36 | @Option(help: """ 37 | The notifications message. 38 | 39 | """) 40 | var message: String = "" 41 | // Optional action 42 | @Option(help: """ 43 | The action to be performed under the users account when the message is clicked. 44 | 45 | • Passing 'logout' will prompt the user to logout. 46 | • If passed a single item, this will be launched via: /usr/bin/open 47 | • More complex commands can be passed, but the 1st argument needs to be a binaries path. 48 | 49 | For example: \"/usr/bin/open\" will work, \"open\" will not. 50 | 51 | """) 52 | var messageaction: String = "" 53 | // Optional message button text 54 | @Option(help: """ 55 | Adds a button to the message, with the label being what is passed. 56 | 57 | """) 58 | var messagebutton: String = "" 59 | // Optional action when the alert button is clicked 60 | @Option(help: """ 61 | The action to be performed under the users account when the optional message button is clicked. 62 | 63 | • Passing 'logout' will prompt the user to logout. 64 | • If passed a single item, this will be launched via: /usr/bin/open 65 | • More complex commands can be passed, but the 1st argument needs to be a binaries path. 66 | 67 | For example: \"/usr/bin/open\" will work, \"open\" will not. 68 | 69 | """) 70 | var messagebuttonaction: String = "" 71 | // Triggers rebrand function 72 | @Option(help: """ 73 | Requires root privileges and that the calling process needs either Full Disk Access (10.15+) or at \ 74 | a minimum App Management (macOS 13+) permissions, as well as the notifying applications being given \ 75 | permission to post to Notification Center. Any of these permissions can be granted manually, but \ 76 | ideally via PPPCP's delivered via an MDM. 77 | 78 | If successful and someone is logged in, Notification Center is restarted. 79 | 80 | """) 81 | var rebrand: String = "" 82 | // Option to remove a specific notification or all notifications delivered 83 | @Option(help: """ 84 | \"prior\" or \"all\". If passing \"prior\", the full message will be required too. \ 85 | Including all passed flags. 86 | 87 | """) 88 | var remove: String = "" 89 | // Optional sound to play when notification is delivered 90 | @Option(help: """ 91 | The sound to play when notification is delivered. Pass \"default\" for the default \ 92 | macOS sound, else the name of a sound in /Library/Sounds or /System/Library/Sounds. 93 | 94 | If the sound cannot be found, macOS will use the \"default\" sound. 95 | 96 | """) 97 | var sound: String = "" 98 | // Optional subtitle for the notification 99 | @Option(help: """ 100 | The notifications subtitle. 101 | 102 | """) 103 | var subtitle: String = "" 104 | // Optional Title for the notification 105 | @Option(help: """ 106 | The notifications title. 107 | 108 | """) 109 | var title: String = "" 110 | // Enables verbose logging 111 | @Flag(help: """ 112 | Enables logging of actions. Check console for 'Notifier' messages. 113 | 114 | """) 115 | var verbose = false 116 | } 117 | -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Notifier - 16x16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Notifier - 32x32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Notifier - 32x32 1.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Notifier - 64x64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Notifier - 128x128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Notifier - 256x256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Notifier - 256x256 1.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Notifier - 512x512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Notifier - 512x512 1.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Notifier - 1024x1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 1024x1024.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 128x128.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 16x16.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256 1.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 256x256.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32 1.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 32x32.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512 1.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 512x512.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamf/Notifier/6b3cd082b89018f771062ff9bca19113c4db50e0/Notifier/Notifier/Assets.xcassets/AppIcon.appiconset/Notifier - 64x64.png -------------------------------------------------------------------------------- /Notifier/Notifier/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Notifier/Notifier/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Notifier/Notifier/Functions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Functions.swift 3 | // Notifier 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import Cocoa 10 | import SystemConfiguration 11 | 12 | // Changes the .app's icons restarting Notification Center if rebranding was successful 13 | func changeIcons(brandingImage: String, loggedInUser: String, parsedResult: ArgParser) { 14 | // Confirm we're root before proceeding 15 | rootCheck(parsedResult: parsedResult, passedArg: "--rebrand") 16 | // If verbose mode is enabled 17 | if parsedResult.verbose { 18 | // Progress log 19 | NSLog("\(#function.components(separatedBy: "(")[0]) - brandingImage: \(brandingImage)") 20 | } 21 | // Get the details of the file at brandingImage 22 | let imageData = getImageDetails(brandingImage: brandingImage, parsedResult: parsedResult) 23 | // For each application in brandingArray 24 | for notifierApp in [GlobalVariables.mainAppPath, GlobalVariables.alertAppPath, GlobalVariables.bannerAppPath] { 25 | // Rebrand each app starting ewith the Notifier.app, this way if this fails the rest are skipped 26 | updateIcon(brandingImage: brandingImage, imageData: imageData!, objectPath: notifierApp, 27 | parsedResult: parsedResult) 28 | } 29 | // If verbose mode is enabled 30 | if parsedResult.verbose { 31 | // Progress log 32 | NSLog("\(#function.components(separatedBy: "(")[0]) - Successfully rebranded Notifier") 33 | } 34 | // If someone is logged in 35 | if loggedInUser != "" { 36 | // If we're logged in and/or Notification Center is runnning register the applications with Notification Center 37 | registerApplications(parsedResult: parsedResult) 38 | // If no-one is logged in 39 | } else { 40 | // If verbose mode is enabled 41 | if parsedResult.verbose { 42 | // Progress log 43 | NSLog("\(#function.components(separatedBy: "(")[0]) - Skipping registration as not logged in") 44 | } 45 | } 46 | // Exit 47 | exit(0) 48 | } 49 | 50 | // Create JSON from passed string 51 | func createJSON(messageContent: MessageContent, parsedResult: ArgParser, rootElements: RootElements) -> String { 52 | // Var declaration 53 | var contentJSON = Data() 54 | var fullJSON = Data() 55 | var rootContent = rootElements 56 | // If verbose mode is enabled 57 | if parsedResult.verbose { 58 | // Progress log 59 | NSLog("\(#function.components(separatedBy: "(")[0]) - messageContent: \(messageContent)") 60 | } 61 | // Try to convert messageContent to JSON 62 | do { 63 | // Create a JSONEncoder object 64 | let jsonEncoder = JSONEncoder() 65 | // Set formatting to sortedKeys - to make sure that the base64 is static 66 | jsonEncoder.outputFormatting = .sortedKeys 67 | // Turn messageContent into JSON 68 | contentJSON = try jsonEncoder.encode(messageContent) 69 | // If encoding into JSON fails 70 | } catch { 71 | // Post error 72 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: error.localizedDescription, functionName: 73 | #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 74 | } 75 | // If verbose mode is enabled 76 | if parsedResult.verbose { 77 | // Progress log 78 | NSLog(""" 79 | \(#function.components(separatedBy: "(")[0]) - contentJSON: \(String(data: contentJSON, encoding: .utf8)!) 80 | """) 81 | } 82 | // Add to contentJSON, but base64 encoded 83 | rootContent.messageContent = contentJSON.base64EncodedString() 84 | // Try to convert jsonContent to JSON 85 | do { 86 | // Create a JSONEncoder object 87 | let jsonEncoder = JSONEncoder() 88 | // Set formatting to sortedKeys - to make sure that the base64 is static 89 | jsonEncoder.outputFormatting = .sortedKeys 90 | // Turn jsonContent into JSON 91 | fullJSON = try jsonEncoder.encode(rootContent) 92 | // If encoding into JSON fails 93 | } catch { 94 | // Post error 95 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: error.localizedDescription, functionName: 96 | #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 97 | } 98 | // If verbose mode is enabled 99 | if parsedResult.verbose { 100 | // Progress log 101 | NSLog("\(#function.components(separatedBy: "(")[0]) - rootJSON: \(String(data: fullJSON, encoding: .utf8)!)") 102 | } 103 | // Return fullJSON, base64 encoded 104 | return fullJSON.base64EncodedString() 105 | } 106 | 107 | // Gets the modification date and time of the file and return as epoch 108 | func getImageDetails(brandingImage: String, parsedResult: ArgParser) -> (NSImage?) { 109 | // Var declaration 110 | var imageData: NSImage? 111 | // If the file exists 112 | if FileManager.default.fileExists(atPath: brandingImage) { 113 | // Create imageData from the file passed to brandingImage 114 | imageData = NSImage(contentsOfFile: brandingImage) 115 | // If imageData isValid is nil, then brandingImage is not a valid icon 116 | if (imageData?.isValid) == nil { 117 | // Post error 118 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "\(brandingImage) is not a valid image...", 119 | functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 120 | // Exit 121 | exit(1) 122 | } 123 | // Return the image data 124 | return imageData 125 | // If the file doesn't exist 126 | } else { 127 | // Post error 128 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: "Cannot locate: \(brandingImage)....", functionName: 129 | #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 130 | // Exit 131 | exit(1) 132 | } 133 | } 134 | 135 | // Checks that notification center is running, and exit if it's not 136 | func isNotificationCenterRunning(parsedResult: ArgParser) { 137 | // Exit if Notification Center isn't running for the user 138 | guard !NSRunningApplication.runningApplications(withBundleIdentifier: 139 | "com.apple.notificationcenterui").isEmpty else { 140 | // Post warning 141 | postToNSLogAndStdOut(logLevel: "WARNING", logMessage: "Notification Center is not running...", 142 | functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 143 | // Exit 144 | exit(1) 145 | } 146 | // If verbose mode is enabled 147 | if parsedResult.verbose { 148 | // Progress log 149 | NSLog("\(#function.components(separatedBy: "(")[0]) - Notification Center is running") 150 | } 151 | } 152 | 153 | // Returns username if a user is logged in, and "" if at the loginwindow 154 | func loggedInUser() -> String { 155 | // Get the name of the logged in user 156 | let loggedInUser = SCDynamicStoreCopyConsoleUser(nil, nil, nil)! as String 157 | // If no-one or loginwindow is returned 158 | if loggedInUser == "loginwindow" || loggedInUser == "" { 159 | return "" 160 | // Else if we have someone logged in 161 | } else { 162 | // Return their username 163 | return loggedInUser 164 | } 165 | } 166 | 167 | // Parses the action text, splitting it into argv compliant format 168 | func parseAction(actionString: String, parsedResult: ArgParser) -> [MessageContent.TaskObject] { 169 | // Var declaration 170 | var regexDict = [String: String]() 171 | var taskArguments = [String]() 172 | var taskPath = String() 173 | var tempActionString = actionString 174 | // If verbose mode is enabled 175 | if parsedResult.verbose { 176 | // Progress log 177 | NSLog("\(#function.components(separatedBy: "(")[0]) - actionString: \(actionString)") 178 | } 179 | // The regex pattern we're to use 180 | let regexPattern = try? NSRegularExpression(pattern: "\'(.*?)\'|\"(.*?)\"") 181 | // Apply pattern to actionText, returning matches 182 | let regexMatches = regexPattern?.matches(in: actionString, options: [], 183 | range: NSRange(location: 0, length: actionString.utf16.count)) 184 | // Iterate over each match 185 | for regexMatch in regexMatches! { 186 | // Set the range 187 | let regexRange = regexMatch.range(at: 1) 188 | // Create a Range object 189 | if let swiftRange = Range(regexRange, in: actionString) { 190 | // Create key in regex dict with the and %20 escape spaces 191 | regexDict[actionString[swiftRange].description] = 192 | actionString[swiftRange].description.replacingOccurrences(of: " ", with: "%20") 193 | } 194 | } 195 | // If we have items in regexDict 196 | if !regexDict.isEmpty { 197 | // For each key value pair we have in regexDict 198 | for (matchedKey, matchedValue) in regexDict { 199 | // Replace the occurences of matchedKey with matchedValue 200 | tempActionString = tempActionString.replacingOccurrences(of: matchedKey, with: matchedValue) 201 | } 202 | // Set taskArguments to the amended string 203 | taskArguments = Array(tempActionString.components(separatedBy: " ")) 204 | // Iterate over the array elements which contain %20 205 | for arrayElement in taskArguments where arrayElement.contains("%20") { 206 | // Get the index of the element 207 | let arrayIndex = taskArguments.firstIndex(of: arrayElement) 208 | // Update the element in the array, removing %20, single and double quotes 209 | taskArguments[arrayIndex!] = arrayElement.replacingOccurrences(of: "%20", with: " ") 210 | .replacingOccurrences(of: "\'", with: "").replacingOccurrences(of: "\"", with: "") 211 | } 212 | // If regexDict is empty 213 | } else { 214 | // Set taskArguments to the passed action 215 | taskArguments = Array(tempActionString.components(separatedBy: " ")) 216 | } 217 | // If we only have a single task 218 | if taskArguments.count == 1 { 219 | // If we've been passed logout 220 | if taskArguments[0].lowercased() == "logout" { 221 | // Set the tasks path to open, this is to mimic pre-3.0 behaviour 222 | taskPath = "logout" 223 | // Clear taskArguments 224 | taskArguments = [] 225 | // If one item and not passed logout 226 | } else { 227 | // Set the tasks path to open, this is to mimic pre-3.0 behaviour 228 | taskPath = "/usr/bin/open" 229 | } 230 | // If we have more than one task, the 1st argument starts with / 231 | } else if taskArguments[0].hasPrefix("/") { 232 | // Set the tasks path to the 1st item within the taskArguments 233 | taskPath = taskArguments[0] 234 | // Remove the above from the taskArguments 235 | taskArguments.remove(at: 0) 236 | // If we have more than one task, and the 1st argument does not start with / 237 | } else { 238 | // Post warning 239 | postToNSLogAndStdOut(logLevel: "WARNING", logMessage: """ 240 | \(taskArguments[0]) is not a path to a binary, not adding to notification... 241 | """, functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 242 | // Return an empty TaskObject 243 | return [MessageContent.TaskObject(taskPath: "", taskArguments: [])] 244 | } 245 | // If verbose mode is enabled 246 | if parsedResult.verbose { 247 | // Progress log 248 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)") 249 | } 250 | // Return a TaskObject with taskPath and taskArguments set 251 | return [MessageContent.TaskObject(taskPath: taskPath, taskArguments: taskArguments)] 252 | } 253 | 254 | // Passes messageContentJSON to the relevant app, exiting afterwards 255 | func passToApp(commandJSON: String, loggedInUser: String, notifierPath: String, parsedResult: ArgParser) { 256 | // Var declaration 257 | var taskArguments = [String]() 258 | var taskPath = String() 259 | // If the user running the app isn't the logged in user (root for example) 260 | if NSUserName() != loggedInUser { 261 | // Set taskPath to su as we need to use that to run as the user 262 | taskPath = "/usr/bin/su" 263 | // Create taskArguments 264 | taskArguments = [ 265 | "-l", "\(loggedInUser)", "-c", "\'\(notifierPath)\' \(commandJSON)" 266 | ] 267 | // If the person running the app is the logged in user 268 | } else { 269 | // Set taskPath to the notifying apps path 270 | taskPath = notifierPath 271 | // Set taskArguments to the base64 of messageContentJSON 272 | taskArguments = [commandJSON] 273 | } 274 | // If verbose mode is enabled 275 | if parsedResult.verbose { 276 | // Progress log 277 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)") 278 | } 279 | // Launch the wanted notification app as the user 280 | runTask(parsedResult: parsedResult, taskArguments: taskArguments, taskPath: taskPath) 281 | // If we're not rebranding 282 | if parsedResult.rebrand == "" { 283 | // Exit 284 | exit(0) 285 | } 286 | } 287 | 288 | // Post to both NSLog and stdout 289 | func postToNSLogAndStdOut(logLevel: String, logMessage: String, functionName: String, parsedResult: ArgParser) { 290 | // If verbose mode is enabled 291 | if parsedResult.verbose { 292 | // Progress log 293 | NSLog("\(logLevel): \(functionName) - \(logMessage)") 294 | // verbose mode isn't enabled 295 | } else { 296 | // Print to stdout 297 | print("\(logLevel): \(logMessage)") 298 | } 299 | } 300 | 301 | // Registers the notifying applications in Notificaton Center 302 | func registerApplications(parsedResult: ArgParser) { 303 | // Var declaration 304 | var taskArguments = [String]() 305 | var taskPath = String() 306 | // If verbose mode is enabled 307 | if parsedResult.verbose { 308 | // Progress log 309 | NSLog("\(#function.components(separatedBy: "(")[0])") 310 | } 311 | // Get the username of the logged in user 312 | let loggedInUser = loggedInUser() 313 | // Initialize a rootElements object 314 | let rootElements = RootElements(removeOption: "prior") 315 | // Initialize a messageContent object with a message body of a UUID 316 | let messageContent = MessageContent(messageBody: UUID().uuidString) 317 | // Create the JSON to pass to the notifying app 318 | let commandJSON = createJSON(messageContent: messageContent, parsedResult: parsedResult, 319 | rootElements: rootElements) 320 | for notifierPath in [GlobalVariables.alertAppPath + "/Contents/MacOS/Notifier - Alerts", 321 | GlobalVariables.bannerAppPath + "/Contents/MacOS/Notifier - Notifications"] { 322 | // Pass commandJSON to the relevant app, exiting afterwards 323 | passToApp(commandJSON: commandJSON, loggedInUser: loggedInUser, notifierPath: notifierPath, 324 | parsedResult: parsedResult) 325 | // If verbose mode is enabled 326 | if parsedResult.verbose { 327 | // Progress log 328 | NSLog(""" 329 | \(#function.components(separatedBy: "(")[0]) - commandJSON: \(commandJSON), notifierPath: \ 330 | \(notifierPath) 331 | """) 332 | } 333 | } 334 | // If verbose mode is enabled 335 | if parsedResult.verbose { 336 | // Progress log 337 | NSLog("\(#function.components(separatedBy: "(")[0]) - restarting Notification Center") 338 | } 339 | // If we're logged in 340 | if loggedInUser != "" { 341 | // Path for the task 342 | taskPath = "/usr/bin/su" 343 | // Arguments for the task 344 | taskArguments = ["-l", loggedInUser, "-c", "/usr/bin/killall -u \(loggedInUser) NotificationCenter"] 345 | // If verbose mode is enabled 346 | if parsedResult.verbose { 347 | // Progress log 348 | NSLog(""" 349 | \(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments) 350 | """) 351 | } 352 | // Run the task, ignoring returned exit status 353 | runTask(parsedResult: parsedResult, taskArguments: taskArguments, taskPath: taskPath) 354 | // If we're not logged in 355 | } else { 356 | // If verbose mode is enabled 357 | if parsedResult.verbose { 358 | // Progress log 359 | NSLog("\(#function.components(separatedBy: "(")[0]) - not logged in, skipping Notification Center restart") 360 | } 361 | } 362 | } 363 | 364 | // Make sure we're running as root, exit if not 365 | func rootCheck(parsedResult: ArgParser, passedArg: String) { 366 | // If we're not root 367 | if NSUserName() != "root" { 368 | // Post error 369 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: """ 370 | The argument: \(passedArg), requires root privileges, exiting... 371 | """, functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 372 | // Exit 373 | exit(1) 374 | } 375 | } 376 | 377 | // Runs the passed task 378 | func runTask(parsedResult: ArgParser, taskArguments: [String], taskPath: String) { 379 | // If verbose mode is enabled 380 | if parsedResult.verbose { 381 | // Progress log 382 | NSLog("\(#function.components(separatedBy: "(")[0]) - taskPath: \(taskPath), taskArguments: \(taskArguments)") 383 | } 384 | // Create Task 385 | let task = Process() 386 | // Set task to call the binary 387 | task.executableURL = URL(fileURLWithPath: taskPath) 388 | // Set task arguments 389 | task.arguments = taskArguments 390 | // Run the task 391 | try? task.run() 392 | // Wait until task exits 393 | task.waitUntilExit() 394 | } 395 | 396 | // Attempts to update the app passed to objectPath's icon 397 | func updateIcon(brandingImage: String, imageData: NSImage, objectPath: String, parsedResult: ArgParser) { 398 | // Revert the icon, always returns false and this helps the OS realise that ther has been an icon change 399 | NSWorkspace.shared.setIcon(nil, forFile: objectPath, options: NSWorkspace.IconCreationOptions([])) 400 | // Set the icon, returns bool 401 | let rebrandStatus = NSWorkspace.shared.setIcon(imageData, forFile: objectPath, options: 402 | NSWorkspace.IconCreationOptions([])) 403 | // If we have succesfully branded the item at objectPath 404 | if rebrandStatus { 405 | // If verbose mode is enabled 406 | if parsedResult.verbose { 407 | // Progress log 408 | NSLog(""" 409 | \(#function.components(separatedBy: "(")[0]) - Successfully updated icon for \(objectPath), \ 410 | with icon: \(brandingImage) 411 | """) 412 | } 413 | // If we encountered and issue when rebranding... 414 | } else { 415 | // Post error 416 | postToNSLogAndStdOut(logLevel: "ERROR", logMessage: """ 417 | Failed to update icon for \(objectPath), with icon: \(brandingImage). Please make sure that the calling \ 418 | process has the needed App Management or Full Disk Access PPPCP deployed, and try again. 419 | """, functionName: #function.components(separatedBy: "(")[0], parsedResult: parsedResult) 420 | // Exit 421 | exit(1) 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /Notifier/Notifier/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | LSApplicationCategoryType 22 | public.app-category.utilities 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | LSUIElement 26 | 27 | NSHumanReadableCopyright 28 | Copyright © 2024 dataJAR Ltd. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Notifier/Notifier/Notifier.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Notifier/Notifier/Structures.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Structures.swift 3 | // Notifier 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import Foundation 10 | 11 | // Variables to be used globally 12 | struct GlobalVariables { 13 | // Path to the main apps bundle 14 | static let mainAppPath = Bundle.main.bundlePath 15 | // Path to the alert apps bundle 16 | static let alertAppPath = mainAppPath + "/Contents/Helpers/Notifier - Alerts.app" 17 | // Path to the banner apps bundle 18 | static let bannerAppPath = mainAppPath + "/Contents/Helpers/Notifier - Notifications.app" 19 | } 20 | 21 | // Structure of MessageContent 22 | struct MessageContent: Codable { 23 | // Optional - action to perform when the message is clicked 24 | var messageAction: [TaskObject]? 25 | // The notifications message (required) 26 | var messageBody: String? 27 | // Optional - message button label 28 | var messageButton: String? 29 | // Optional - action to perform when the message button is clicked 30 | var messageButtonAction: [TaskObject]? 31 | // Optional - the sound played when the notification has been delivered 32 | var messageSound: String? 33 | // Optional - the notifications subtitle 34 | var messageSubtitle: String? 35 | // Optional - the notifications title 36 | var messageTitle: String? 37 | // Arguments for the task object 38 | struct TaskObject: Codable { 39 | // The tasks executable 40 | var taskPath: String? 41 | // Arguments to pass to the task executable 42 | var taskArguments: [String]? 43 | } 44 | // Initialize MessageContent 45 | init(messageAction: [TaskObject]? = nil, messageBody: String? = nil, messageButton: String? = nil, 46 | messageButtonAction: [TaskObject]? = nil, messageSound: String? = nil, messageSubtitle: String? = nil, 47 | messageTitle: String? = nil) { 48 | self.messageAction = messageAction 49 | self.messageBody = messageBody 50 | self.messageButton = messageButton 51 | self.messageButtonAction = messageButtonAction 52 | self.messageSound = messageSound 53 | self.messageSubtitle = messageSubtitle 54 | self.messageTitle = messageTitle 55 | } 56 | } 57 | 58 | // Structure of RootElements 59 | struct RootElements: Codable { 60 | // Optional - the messages content 61 | var messageContent: String? 62 | // Optional - removes a specific notification or all notifications delivered 63 | var removeOption: String? 64 | // Optional - enables verbose logging 65 | var verboseMode: String? 66 | // Initialize MessageContent 67 | init(messageContent: String? = nil, removeOption: String? = nil, verboseMode: String? = nil) { 68 | self.messageContent = messageContent 69 | self.removeOption = removeOption 70 | self.verboseMode = verboseMode 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Notifier/Notifier/UserNotifications.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserNotifications.swift 3 | // Notifiers - Alerts 4 | // 5 | // Copyright © 2024 dataJAR Ltd. All rights reserved. 6 | // 7 | 8 | // Imports 9 | import UserNotifications 10 | 11 | // Sets the notifications body to what was passed to --message 12 | func setNotificationBody(parsedResult: ArgParser) -> String { 13 | // If verbose mode is enabled 14 | if parsedResult.verbose { 15 | // Progress log 16 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.message)") 17 | } 18 | // Returns the value passed to --message 19 | return parsedResult.message 20 | } 21 | 22 | // Sets the notifications message button to what was passed to --messagebutton 23 | func setNotificationMessageButton(parsedResult: ArgParser) -> String { 24 | // If verbose mode is enabled 25 | if parsedResult.verbose { 26 | // Progress log 27 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.messagebutton)") 28 | } 29 | // Returns the value passed to --messagebutton 30 | return parsedResult.messagebutton 31 | } 32 | 33 | // Sets the notifications sound to what was passed to --sound 34 | func setNotificationSound(parsedResult: ArgParser) -> String { 35 | // If verbose mode is enabled 36 | if parsedResult.verbose { 37 | // Progress log 38 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.sound)") 39 | } 40 | // Returns the value passed to --sound 41 | return parsedResult.sound 42 | } 43 | 44 | // Sets the notifications subtitle to what was passed to --subtitle 45 | func setNotificationSubtitle(parsedResult: ArgParser) -> String { 46 | // If verbose mode is enabled 47 | if parsedResult.verbose { 48 | // Progress log 49 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.subtitle)") 50 | } 51 | // Returns the value passed to --subtitle 52 | return parsedResult.subtitle 53 | } 54 | 55 | // Sets the notifications title to what was passed to --title 56 | func setNotificationTitle(parsedResult: ArgParser) -> String { 57 | // If verbose mode is enabled 58 | if parsedResult.verbose { 59 | // Progress log 60 | NSLog("\(#function.components(separatedBy: "(")[0]): \(parsedResult.title)") 61 | } 62 | // Returns the value passed to --title 63 | return parsedResult.title 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Licensed under the Apache License, Version 2.0 3 | Copyright 2024 Jamf LTD 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 8 |
9 | 10 | # Notifier 11 |

Alert 12 | 13 | Notifier is a Swift app which can post alert or banner notifications on macOS 10.15+ clients. 14 | 15 | Notifications are delivered via the [UserNotifications Framework](https://developer.apple.com/documentation/usernotifications) 16 | 17 | # Usage 18 | ## Basic Usage 19 | ``` 20 | OVERVIEW: Notifier 3.1: Posts alert or banner notifications. 21 | 22 | USAGE: --type --message 23 | --type --remove prior 24 | --type --remove all 25 | --rebrand 26 | 27 | OPTIONS: 28 | --type alert or banner - REQUIRED. 29 | 30 | --message The notifications message. 31 | 32 | --messageaction 33 | The action to be performed under the users account 34 | when the message is clicked. 35 | 36 | • Passing 'logout' will prompt the user to logout. 37 | • If passed a single item, this will be launched via: 38 | /usr/bin/open 39 | • More complex commands can be passed, but the 1st 40 | argument needs to be a binaries path. 41 | 42 | For example: "/usr/bin/open" will work, "open" will 43 | not. 44 | 45 | --messagebutton 46 | Adds a button to the message, with the label being 47 | what is passed. 48 | 49 | --messagebuttonaction 50 | The action to be performed under the users account 51 | when the optional message button is clicked. 52 | 53 | • Passing 'logout' will prompt the user to logout. 54 | • If passed a single item, this will be launched via: 55 | /usr/bin/open 56 | • More complex commands can be passed, but the 1st 57 | argument needs to be a binaries path. 58 | 59 | For example: "/usr/bin/open" will work, "open" will 60 | not. 61 | 62 | --rebrand Requires root privileges and that the calling process 63 | needs either Full Disk Access (10.15+) or at a 64 | minimum App Management (macOS 13+) permissions, as 65 | well as the notifying applications being given 66 | permission to post to Notification Center. Any of 67 | these permissions can be granted manually, but 68 | ideally via PPPCP's delivered via an MDM. 69 | 70 | If successful and someone is logged in, Notification 71 | Center is restarted. 72 | 73 | --remove "prior" or "all". If passing "prior", the full 74 | message will be required too. Including all passed 75 | flags. 76 | 77 | --sound The sound to play when notification is delivered. 78 | Pass "default" for the default macOS sound, else the 79 | name of a sound in /Library/Sounds or 80 | /System/Library/Sounds. 81 | 82 | If the sound cannot be found, macOS will use the 83 | "default" sound. 84 | 85 | --subtitle The notifications subtitle. 86 | 87 | --title The notifications title. 88 | 89 | --verbose Enables logging of actions. Check console for 90 | 'Notifier' messages. 91 | 92 | --help Show help information. 93 | 94 | ``` 95 | 96 | ## Examples 97 | The below aim to give you an idea of what so the various notification options look like, across macOS versions as well as Light and Dark mode. 98 | 99 | As you can see, some of the behaviour and appearence is OS dependent. 100 | 101 | **Example 1** This example shows a basic banner notification. 102 | 103 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode 104 | :-------------------------:|:-------------------------: 105 | <img src="https://github.com/dataJAR/Notifier/assets/2464974/20af5bb9-1e6e-4158-bbc4-67dbd003d7f8" width="317" height="100"> | <img src="https://github.com/dataJAR/Notifier/assets/2464974/bf8a477c-3ebc-4c54-b4ef-eaa6df8dc052" width="317" height="100"> 106 | 107 | ``` 108 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message" 109 | ``` 110 | ## 111 | **Example 2** This example shows a basic alert notification. 112 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode 113 | :-------------------------:|:-------------------------: 114 | <img src="https://github.com/dataJAR/Notifier/assets/2464974/1bca78ef-8865-4762-902b-081ac39b9c80" width="317" height="100"> | <img src="https://github.com/dataJAR/Notifier/assets/2464974/e3f6e325-d7d0-49e8-8bfc-34c19025d472" width="317" height="100"> 115 | 116 | ``` 117 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "message" --messagebutton "Logout" --messagebuttonaction "Logout" 118 | ``` 119 | ## 120 | **Example 3** This example shows both alert & banner notifications, (sleep used for the example gif, not needed in use but below for completeness sake). 121 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode 122 | :-------------------------:|:-------------------------: 123 | <img src="https://github.com/dataJAR/Notifier/assets/2464974/8741ccea-f481-46a0-8f15-b97acde2e0c1" width="317" height="154"> | <img src="https://github.com/dataJAR/Notifier/assets/2464974/dd1f1775-3392-4d24-84b2-dd55aca6d02d" width="317" height="154"> 124 | 125 | ``` 126 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "Please Logout" --messagebutton "Logout" --messagebuttonaction "logout" --title "Logout"; 127 | /bin/sleep 2; 128 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "👋" --title "Notification" 129 | ``` 130 | ## 131 | **Example 4** This example shows selective remove of a delivered notificaition via `--remove prior`. Where applicable this would also remove notifications from within Notification Center itself. 132 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode 133 | :-------------------------:|:-------------------------: 134 | <img src="https://github.com/dataJAR/Notifier/assets/2464974/854440ab-ee52-443b-8d65-ebcad501f194" width="317" height="154"> | <img src="https://github.com/dataJAR/Notifier/assets/2464974/a0e04d63-0e9f-474d-ba8e-1fa97bdf18be" width="317" height="154"> 135 | 136 | ``` 137 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message 'Look at me!!'; 138 | /bin/sleep 2; 139 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "message" --messagebutton "Logout"; 140 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message 'Look at me!!' --remove prior 141 | ``` 142 | ## 143 | **Example 5** This example shows removal of all alert notifications and also the differences of delivery of alert notifications across macOS versions, additionally this removes any prior delivered notificaions from Notification Center. Sleep used for the example gif, not needed in use but below for completeness sake. 144 | 145 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode 146 | :-------------------------:|:-------------------------: 147 | <img src="https://github.com/dataJAR/Notifier/assets/2464974/8f4abba7-7866-4e80-8aad-1bc77dcda50c" width="317" height="374"> | <img src="https://github.com/dataJAR/Notifier/assets/2464974/11902803-3e4e-4ac8-8dda-5c4f0994896e" width="317" height="374"> 148 | 149 | ``` 150 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message; 151 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message1; 152 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message2; 153 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message3; 154 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message4; 155 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message message5; 156 | /bin/sleep 5; 157 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --remove all 158 | ``` 159 | ## 160 | **Example 6** This example shows Notifier 3.0+'s `--rebrand` argument in use, this allows for rebranding of Notifier without having to venture into Xcode etc. 161 | macOS 10.15.7 - Light Mode | macOS 14.3 - Dark mode 162 | :-------------------------:|:-------------------------: 163 | <img src="https://github.com/dataJAR/Notifier/assets/2464974/ea45ad52-96ec-49a1-8679-4d1461451ea5" width="317" height="154"> | <img src="https://github.com/dataJAR/Notifier/assets/2464974/49894a04-17e8-4257-9923-e438418a6785" width="317" height="154"> 164 | 165 | ``` 166 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type alert --message "message"; 167 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message"; 168 | /bin/sleep 2; 169 | /usr/bin/sudo /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --rebrand /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/ErasingIcon.icns; 170 | /bin/sleep 2; 171 | /usr/bin/sudo /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message"; 172 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --rebrand /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericFileServerIcon.icns; 173 | /bin/sleep 2; 174 | /usr/bin/sudo /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message"; 175 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --rebrand /Applications/Utilities/Notifier.app/Contents/Resources/AppIcon.icns; 176 | /bin/sleep 2; 177 | /Applications/Utilities/Notifier.app/Contents/MacOS/Notifier --type banner --message "message" 178 | ``` 179 | 180 | # Deployment 181 | 182 | ## PPPC 183 | It's recommended that the below profile is recommended to be deployed before Notfier itself, to UAMDM devices. 184 | (https://github.com/dataJAR/Notifier/blob/master/profile/Allow%20Notifier%20Notifications.mobileconfig) 185 | 186 | This will allow Notifier to post Notifications without prompting the user to allow. 187 | 188 | Additionally, if you're looking to make use of the `--rebrand` flag the calling process needs either Full Disk Access/SystemPolicyAllFiles (10.15+) or at a 189 | minimum App Management/SystemPolicyAppBundles (macOS 13+) permissions, as well as the notifying applications being given permission to post to Notification Center. 190 | 191 | Any of these permissions can be granted manually, but ideally via PPPCP's delivered via an MDM. 192 | 193 | ## PKG 194 | PKG's will be supplied for every release, & can be found in the [releases](https://github.com/dataJAR/Notifier/releases) section. 195 | 196 | ## Flow 197 | 198 | The below is the advised deployment flow for Notifier. 199 | 200 | 1. If rebranding - deploy a PPPC for your management tool of choice, granting either Full Disk Access/SystemPolicyAllFiles (10.15+) or App Management/SystemPolicyAppBundles (macOS 13+). 201 | 2. Deploy the [Notications PpPC](https://github.com/dataJAR/Notifier/blob/master/profile/Allow%20Notifier%20Notifications.mobileconfig) to UAMDM devices 202 | 3. Deploy the latest [Notifier PKG](https://github.com/dataJAR/Notifier/releases) 203 | 4. If rebranding - deploy the image to use when rebranding. 204 | 5. if rebranding - deploy a script or payload free package etc which rebrands Notifier via your management tool. This needs to be down with admin/root privileges. 205 | 206 | # How it works 207 | The main Notifier.app parses arguments passed to it (via [Argument Parser](https://apple.github.io/swift-argument-parser/documentation/argumentparser/), and then posts the parsed argument to the two notifying applications included with the the /Contents/Helpers folder of Notifier.app: 208 | 209 | ``` 210 | /Applications/Utilities/Notifier.app/Contents/Helpers/ 211 | Notifier - Alerts.app 212 | Notifier - Notifications.app 213 | ``` 214 | Additionally the Notifier.app also checks tha Notification Center is running, & runs the notifying apps in the user context. 215 | 216 | The notifying apps (/Applications/Utilities/Notifier.app/Contents/Helpers/Notifier - Alerts.app and /Applications/Utilities/Notifier.app/Contents/Helpers/Notifier - Notifications.app) both post notifications via the [UserNotifications Framework](https://developer.apple.com/documentation/usernotifications). 217 | 218 | # Miscellaneous 219 | 220 | ## Resetting Notifications Center 221 | The below _should_ reset Notifications Center, but please test & submit a PR with a better method as applicable. 222 | 223 | 1. `/bin/rm -rf $(getconf DARWIN_USER_DIR)/com.apple.notificationcenter/*` 224 | 2. Logout & then log back in, or: 225 | * For macOS 10.15+: `/usr/bin/killall "NotificationCenter"` 226 | 3. Test the Notifier once more 227 | 228 | # FAQs 229 | 230 | **Q1:** Why start at version 2? 231 | 232 | **A1:** At dataJAR, we already had a v1 "Notifier" app deployed. 233 | ## 234 | **Q2:** How can you post both banner & alert notifications? 235 | 236 | **A2:** See [How it works](#how-it-works) 237 | ## 238 | **Q3:** The users are being prompted to allow.. help! 239 | 240 | **A3:** This can be due to a few things: 241 | 242 | 1. Was the notifications profile installed? If not, install. 243 | 1. Is the device under UAMDM? If not, check with your MDM vendor on making the device UAMDM. Then try again & maybe reinstall the profile once under UAMDM. 244 | 1. Did you change the Bundle ID of either the Alert or Banner applications? If so you'll need to amend the notifications profile accordingly. 245 | ## 246 | **Q4:** Is that logo magen... 247 | 248 | **A4:** No, it's a shade of pink... 249 | ## 250 | **Q5:** Banner or Notifications? 251 | 252 | **A5:** The temporary notifications are referred to as "banner" notifications in Apple's documentation, but it seems that most folks call them "Notifications" with the persistent notifications being referred to as alerts by folks as well as within Apple's documentation. 253 | ## 254 | **Q6:** I'm seeing alerts when expecting banner notifications, & vice versa 255 | 256 | **A6:** Check the Notification settings within System Preferences/System Settings, it's possible that the incorrect option has been selected or set via a profile. 257 | ## 258 | **Q7:** --remove prior, didn't clear my last message. 259 | 260 | **A7:** Make sure you pass EXACTLY the same message to be cleared. 261 | ## 262 | **Q8:** I've rebranded, but the old icon is being shown.. 263 | 264 | **A8:** Please reset Notification Center as per the [Resetting Notifications Center](#resetting-notifications-center), then try again. 265 | ## 266 | **Q9:** Does rebranding work when no one is logged in? 267 | 268 | **A9:** Yep, and no restart of Notification Center is needed. 269 | ## 270 | **Q10** Why are notifcations all of a sudden showing a prohibted sign across the icon? 271 | 272 | **A10** This is a [macOS issue](https://macmule.com/2021/10/28/notifications-showing-a-prohibitory-symbol-after-upgrading-macos-monterey/) that has goes back some years, to resolve either restart Notification Center or the Mac. 273 | 274 | # Alternatives 275 | The below projects can be used instead of Notifier & were very much instrumental in Notifiers creation 276 | 277 | ## Alert Notifications 278 | [Yo](https://github.com/sheagcraig/yo) 279 | 280 | ## Banner Notifications 281 | [Terminal Notifier](https://github.com/julienXX/terminal-notifier) 282 | 283 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: datajar 5 | annotations: 6 | sonarqube.org/project-key: com.jamf.team-red-dawg:Notifier 7 | spec: 8 | type: service 9 | owner: datajar 10 | lifecycle: production 11 | system: datajar 12 | -------------------------------------------------------------------------------- /package/make-notifier-pkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ########################################################################################## 4 | # 5 | # In accessing and using this Product, you confirm that you accept the terms of our 6 | # Product Licence in full and agree to comply with the terms of such Licence. 7 | # 8 | # https://datajar.co.uk/product-licence/ 9 | # 10 | ########################################################################################## 11 | # 12 | # CHANGE LOG 13 | # 1.0 - Created 14 | # 15 | ########################################################################################## 16 | # 17 | # This script creates Notifier-<version>.pkg within this repos package dir. 18 | # 19 | # Needs to be ran as root/sudo, and requires the following arguments to be passed to it: 20 | # 21 | # $1 - The team id of a "Developer ID Installer" identity within the running users keychain: 22 | # https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates/ 23 | # $2 - Name of an App-specific password within the running users keychain, to be used for notarization 24 | # https://support.apple.com/en-us/102654 25 | # $3 - pkg receipt identifier 26 | # uk.dataJAR.Notifier 27 | # 28 | ########################################################################################## 29 | ################################### Global Variables ##################################### 30 | ########################################################################################## 31 | 32 | # Exit if any commands error 33 | set -e 34 | 35 | # Extension of the script (.py, .sh etc).. 36 | scriptExtension=${0##*.} 37 | 38 | # Overall name of the family of software we are installing, with extension removed 39 | swTitle="$(/usr/bin/basename "$0" ."$scriptExtension")" 40 | 41 | # Script Version 42 | ver="1.0.1" 43 | 44 | # Full path to the dir this script is in 45 | scriptDir="$(/usr/bin/dirname "$0")" 46 | 47 | # Path to Notifier.app 48 | notifierPath="${scriptDir}/ROOT/Applications/Utilities/Notifier.app" 49 | 50 | # Path to scripts dir the packages dir 51 | pkgScriptsDir="${scriptDir}/scripts/" 52 | 53 | # Developer ID Installer identity 54 | teamID="${1}" 55 | 56 | # App specific password for notarization 57 | appPassword="${2}" 58 | 59 | # pkg identifier 60 | pkgIdentifier="${3}" 61 | 62 | ########################################################################################## 63 | #################################### Start functions ##################################### 64 | ########################################################################################## 65 | 66 | setup() 67 | { 68 | # Make sure we're root 69 | if [ "$(id -u)" != "0" ] 70 | then 71 | /bin/echo "ERROR: This script must be run as root" 72 | return 1 73 | fi 74 | 75 | # Make sure that there is a Notifier.app in the package folder 76 | if [ ! -e "${notifierPath}" ] 77 | then 78 | /bin/echo "ERROR: Notifier.app not found at: ${notifierPath}, exiting..." 79 | return 1 80 | fi 81 | 82 | # Get the value of $1, error if doesn't exist 83 | if [ -z "${teamID}" ] 84 | then 85 | /bin/echo "ERROR: teamID argument passed to script..." 86 | /bin/echo 87 | return 1 88 | # If we have been passed an value to $1 89 | else 90 | # Make sure that we have a "Developer ID Installer: ${teamdID}" identity in the users keychain 91 | developerIDInstaller=$(/usr/bin/security find-identity -v 'login.keychain' -s -p basic | /usr/bin/awk -F "\"" '/Developer ID Installer:.*?'"${teamID}"'/ {print $2}') 92 | # If we haven't found the required Developer ID Installer 93 | if [ -z "${developerIDInstaller}" ] 94 | then 95 | # Error if we cannot find Developer ID Installer: ${teamdID} in the keychain 96 | /bin/echo "ERROR: no \"${teamID}\" Developer ID Installer certificate found in login.keychain, exiting..." 97 | return 1 98 | fi 99 | fi 100 | 101 | # Get the value of $2, error if doesn't exist 102 | if [ -z "${appPassword}" ] 103 | then 104 | /bin/echo "ERROR: missing argument $2..." 105 | /bin/echo 106 | return 1 107 | # If we have been passed an value to $2 108 | else 109 | # Make sure that $appPassword exists in the user keychain 110 | if [ -z "$(/usr/bin/security find-generic-password login.keychain -D "application password" -G "${appPassword}")" ] 111 | then 112 | # Error if we cannot find appPassword in the keychain 113 | /bin/echo "ERROR: password: \"${appPassword}\" not found in login.keychain, exiting..." 114 | return 1 115 | fi 116 | fi 117 | 118 | # Get the value of $3, error if doesn't exist 119 | if [ -z "${pkgIdentifier}" ] 120 | then 121 | /bin/echo "ERROR: missing argument $3..." 122 | /bin/echo 123 | return 1 124 | fi 125 | } 126 | 127 | 128 | start() 129 | { 130 | # Logging start 131 | /bin/echo "Running ${swTitle} ${ver}..." 132 | } 133 | 134 | 135 | finish() 136 | { 137 | # Logging finish 138 | /bin/echo "Finished: $(/bin/date)" 139 | } 140 | 141 | usage() 142 | { 143 | /bin/echo " 144 | ABOUT: This script creates Notifier-<version>.pkg within this repos package dir. 145 | 146 | USAGE: Ths script needs to be ran as root/sudo, and requires the following arguments to be passed to it: 147 | 148 | \$1 - The team id of a "Developer ID Installer" identity within the running users keychain: 149 | https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates/ 150 | \$2 - Name of an App-specific password within the running users keychain, to be used for notarization: 151 | https://support.apple.com/en-us/102654 152 | \$3 - pkg receipt identifier (for example: uk.dataJAR.Notifier) 153 | " 154 | } 155 | 156 | getNotifierVersion() 157 | { 158 | # Queries Notifier.app for version 159 | /bin/echo "Getting notifier version from ${notifierPath}/Contents/Info.plist..." 160 | notifierVersion=$(/usr/bin/defaults read "${notifierPath}"/Contents/Info.plist CFBundleShortVersionString) 161 | /bin/echo "notifier version: ${notifierVersion}..." 162 | } 163 | 164 | 165 | getMinOSVersion() 166 | { 167 | # Queries Notifier.app for min os version 168 | /bin/echo "Getting min os version from ${notifierPath}/Contents/Info.plist..." 169 | minOS=$(/usr/bin/defaults read "${notifierPath}"/Contents/Info.plist LSMinimumSystemVersion) 170 | /bin/echo "minOS: ${minOS}..." 171 | } 172 | 173 | setPermissions() 174 | { 175 | # Sets user:group ownership 176 | /bin/echo "Setting root:wheel for: ${scriptDir}/ROOT/..." 177 | /usr/sbin/chown -R root:wheel "${scriptDir}/ROOT/" 178 | 179 | # Set perms 180 | /bin/echo "Setting 755 permissions for: ${scriptDir}/ROOT/..." 181 | /bin/chmod -R 755 "${scriptDir}/ROOT/" 182 | 183 | # Mark Notifier.app as executable 184 | /bin/echo "Marking: ${notifierPath} as executable..." 185 | /usr/bin/sudo /bin/chmod -R +x "${notifierPath}" 186 | } 187 | 188 | createPKG() 189 | { 190 | # Creates a component plist 191 | /bin/echo "Creating ${scriptDir}/notifier-app.plist"... 192 | /usr/bin/pkgbuild --analyze --root "${scriptDir}"/ROOT/ "${scriptDir}"/notifier-app.plist 193 | 194 | # Set BundleIsRelocatable to NO in component plist 195 | /bin/echo "Setting \"BundleIsRelocatable\" to NO in ${scriptDir}/notifier-app.plist..." 196 | /usr/bin/plutil -replace BundleIsRelocatable -bool NO "${scriptDir}"/notifier-app.plist 197 | 198 | # Creates notifier.pkg in ${scriptDir}/ 199 | /bin/echo "Creating ${scriptDir}/notifier-${notifierVersion}.pkg" 200 | /usr/bin/pkgbuild --identifier "${pkgIdentifier}" --root "${scriptDir}"/ROOT/ --sign "${developerIDInstaller}" --install-location '/' --version "${notifierVersion}" --min-os-version "${minOS}" --component-plist "${scriptDir}"/notifier-app.plist "${scriptDir}"/Notifier-"${notifierVersion}".pkg 201 | } 202 | 203 | notarizePKG() 204 | { 205 | # Notarizes the PKG created within the createPKG function 206 | /bin/echo "Notarizing ${scriptDir}/Notifier-${notifierVersion}.pkg..." 207 | /usr/bin/xcrun notarytool submit "${scriptDir}"/Notifier-"${notifierVersion}".pkg --keychain-profile "${appPassword}" --wait 208 | } 209 | 210 | clearPermissions() 211 | { 212 | # Sets perms on ROOT dir recursively after building a pkg 213 | /bin/echo "Clearing permissions on: ${scriptDir}/ROOT/..." 214 | /bin/chmod -R 777 "${scriptDir}/ROOT/" 215 | } 216 | 217 | ########################################################################################## 218 | #################################### End functions ####################################### 219 | ########################################################################################## 220 | 221 | if ! setup "$@" 222 | then 223 | usage 224 | else 225 | start 226 | getNotifierVersion 227 | getMinOSVersion 228 | setPermissions 229 | createPKG 230 | notarizePKG 231 | clearPermissions 232 | finish 233 | fi 234 | -------------------------------------------------------------------------------- /profile/Allow Notifier Notifications.mobileconfig: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 | <plist version="1.0"> 4 | <dict> 5 | <key>PayloadContent</key> 6 | <array> 7 | <dict> 8 | <key>NotificationSettings</key> 9 | <array> 10 | <dict> 11 | <key>AlertType</key> 12 | <integer>1</integer> 13 | <key>BadgesEnabled</key> 14 | <true/> 15 | <key>BundleIdentifier</key> 16 | <string>uk.co.dataJAR.NotifierNotifications</string> 17 | <key>CriticalAlertEnabled</key> 18 | <false/> 19 | <key>NotificationsEnabled</key> 20 | <true/> 21 | <key>ShowInLockScreen</key> 22 | <true/> 23 | <key>ShowInNotificationCenter</key> 24 | <true/> 25 | <key>SoundsEnabled</key> 26 | <true/> 27 | </dict> 28 | <dict> 29 | <key>AlertType</key> 30 | <integer>2</integer> 31 | <key>BadgesEnabled</key> 32 | <true/> 33 | <key>BundleIdentifier</key> 34 | <string>uk.co.dataJAR.NotifierAlerts</string> 35 | <key>CriticalAlertEnabled</key> 36 | <false/> 37 | <key>NotificationsEnabled</key> 38 | <true/> 39 | <key>ShowInLockScreen</key> 40 | <true/> 41 | <key>ShowInNotificationCenter</key> 42 | <true/> 43 | <key>SoundsEnabled</key> 44 | <true/> 45 | </dict> 46 | </array> 47 | <key>PayloadDescription</key> 48 | <string></string> 49 | <key>PayloadDisplayName</key> 50 | <string>Allow Notifier Notifications</string> 51 | <key>PayloadEnabled</key> 52 | <true/> 53 | <key>PayloadIdentifier</key> 54 | <string>com.github.erikberglund.ProfileCreator.640EBF16-58E0-42CE-83D3-9BC3A2A6010E.com.apple.notificationsettings.86ADF17C-0236-44C6-B2C2-00037C0C1CC3</string> 55 | <key>PayloadOrganization</key> 56 | <string>dataJAR Ltd</string> 57 | <key>PayloadType</key> 58 | <string>com.apple.notificationsettings</string> 59 | <key>PayloadUUID</key> 60 | <string>86ADF17C-0236-44C6-B2C2-00037C0C1CC3</string> 61 | <key>PayloadVersion</key> 62 | <integer>1</integer> 63 | </dict> 64 | </array> 65 | <key>PayloadDescription</key> 66 | <string></string> 67 | <key>PayloadDisplayName</key> 68 | <string>Allow Notifier Notifications</string> 69 | <key>PayloadEnabled</key> 70 | <true/> 71 | <key>PayloadIdentifier</key> 72 | <string>C4B6FAAA-D592-4FA2-BF68-F8994C05FF0D</string> 73 | <key>PayloadOrganization</key> 74 | <string>dataJAR Ltd</string> 75 | <key>PayloadRemovalDisallowed</key> 76 | <true/> 77 | <key>PayloadScope</key> 78 | <string>System</string> 79 | <key>PayloadType</key> 80 | <string>Configuration</string> 81 | <key>PayloadUUID</key> 82 | <string>C4B6FAAA-D592-4FA2-BF68-F8994C05FF0D</string> 83 | <key>PayloadVersion</key> 84 | <integer>1</integer> 85 | </dict> 86 | </plist> 87 | --------------------------------------------------------------------------------