├── .gitignore ├── .idea ├── .gitignore ├── .name ├── AirMessage.iml ├── codeStyles │ └── codeStyleConfig.xml ├── dataSources.xml ├── misc.xml ├── modules.xml ├── runConfigurations │ └── AirMessage.xml ├── vcs.xml └── xcode.xml ├── AirMessage.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── cole.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── AirMessage ├── AirMessage-Bridging-Header.h ├── AirMessage.entitlements ├── AppDelegate.swift ├── AppleScript │ ├── AppleScriptBridge.swift │ ├── AppleScriptCodes.swift │ └── AppleScriptSource │ │ ├── Common │ │ ├── pressCommandReturn.applescript │ │ └── testPermissionsAutomation.applescript │ │ ├── FaceTime │ │ ├── acceptPendingUser.applescript │ │ ├── centerWindow.applescript │ │ ├── getActiveLink.applescript │ │ ├── getNewLink.applescript │ │ ├── handleIncomingCall.applescript │ │ ├── initiateOutgoingCall.applescript │ │ ├── leaveCall.applescript │ │ ├── queryIncomingCall.applescript │ │ └── queryOutgoingCall.applescript │ │ └── Messages │ │ ├── createChat.applescript │ │ ├── sendMessageDirect.applescript │ │ ├── sendMessageExisting.applescript │ │ ├── sendMessageNew.applescript │ │ └── testPermissionsMessages.applescript ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AccessibilityAccess-13.imageset │ │ ├── AccessibilityAccess-13.png │ │ └── Contents.json │ ├── AccessibilityAccess.imageset │ │ ├── AccessibilityAccess.png │ │ └── Contents.json │ ├── Android.imageset │ │ ├── Android.png │ │ ├── Android2.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── @1024Xcode asset.png │ │ ├── @128Xcode asset.png │ │ ├── @16Xcode asset.png │ │ ├── @256Xcode asset-1.png │ │ ├── @256Xcode asset.png │ │ ├── @32Xcode asset-1.png │ │ ├── @32Xcode asset.png │ │ ├── @512Xcode asset-1.png │ │ ├── @512Xcode asset.png │ │ ├── @64Xcode asset.png │ │ └── Contents.json │ ├── AppIconResource.imageset │ │ ├── @1024Xcode asset-1.png │ │ ├── @256Xcode asset.png │ │ ├── @512Xcode asset-1.png │ │ └── Contents.json │ ├── AutomationAccess-13.imageset │ │ ├── AutomationAccess-13.png │ │ └── Contents.json │ ├── AutomationAccess.imageset │ │ ├── AutomationAccess.png │ │ └── Contents.json │ ├── BrowserChrome.imageset │ │ ├── BrowserChrome.png │ │ ├── Contents.json │ │ └── diagram.png │ ├── BrowserEdge.imageset │ │ ├── BrowserEdge.png │ │ ├── Contents.json │ │ └── Edge2.png │ ├── BrowserFirefox.imageset │ │ ├── BrowserFirefox.png │ │ ├── Contents.json │ │ └── diagram.png │ ├── BrowserOpera.imageset │ │ ├── BrowserOpera.png │ │ ├── Contents.json │ │ └── diagram.png │ ├── BrowserSafari.imageset │ │ ├── BrowserSafari.png │ │ ├── Contents.json │ │ └── diagram.png │ ├── BrowserSamsung.imageset │ │ ├── BrowserSamsung.png │ │ ├── Contents.json │ │ └── samsung.png │ ├── Contents.json │ ├── FullDiskAccess-13.imageset │ │ ├── Contents.json │ │ └── FullDiskAccess-13.png │ ├── FullDiskAccess.imageset │ │ ├── Contents.json │ │ └── FullDiskAccess.png │ ├── StatusBarIcon.imageset │ │ ├── Contents.json │ │ └── StatusBarIcon.png │ └── Windows.imageset │ │ ├── Contents.json │ │ ├── Windows.png │ │ └── Windows2.png ├── Base.lproj │ └── Main.storyboard ├── Compat │ └── FileHandleCompat.swift ├── Connection │ ├── AdvancedAttachmentsFilter.swift │ ├── ClientConnection.swift │ ├── CommConst.swift │ ├── Connect │ │ ├── ConnectConstants.swift │ │ └── DataProxyConnect.swift │ ├── ConnectionManager.swift │ ├── DataProxy.swift │ ├── DataProxyDelegate.swift │ ├── Direct │ │ ├── ClientConnectionTCP.swift │ │ └── DataProxyTCP.swift │ ├── EncryptionManager.swift │ └── FileDownloadRequest.swift ├── Constants │ ├── AccountType.swift │ ├── CustomQueue.swift │ ├── NotificationNames.swift │ ├── ServerState.swift │ └── ServerStateRecovery.swift ├── Controllers │ ├── AccessibilityAccessViewController.swift │ ├── AccountConnectViewController.swift │ ├── AutomationAccessViewController.swift │ ├── ClientListViewController.swift │ ├── FullDiskAccessViewController.swift │ ├── OnboardingViewController.swift │ ├── PasswordEntryViewController.swift │ ├── PreferencesViewController.swift │ ├── SoftwareUpdateProgressViewController.swift │ └── SoftwareUpdateViewController.swift ├── Data │ ├── DBFetchGrouping.swift │ ├── MessageTypes.swift │ ├── UpdateErrorCode.swift │ └── UpdateStruct.swift ├── Database │ ├── DatabaseConverter.swift │ ├── DatabaseManager.swift │ ├── DatabaseTimeConverter.swift │ └── SQL │ │ ├── QueryAllChatDetails.sql │ │ ├── QueryAllChatDetailsSince.sql │ │ ├── QueryAllChatSummary.sql │ │ ├── QueryMessageChatHandle.sql │ │ ├── QueryOutgoingMessages.sql │ │ └── QuerySpecificChatDetails.sql ├── Helper │ ├── ArchiveHelper.swift │ ├── AssertionHelper.swift │ ├── AtomicValue.swift │ ├── CompressionHelper.swift │ ├── ContentTypeHelper.swift │ ├── CryptoHelper.swift │ ├── DispatchHelper.swift │ ├── FaceTimeHelper.swift │ ├── FileNormalizationHelper.swift │ ├── FirebaseAuthHelper.swift │ ├── KeychainManager.swift │ ├── LogManager.swift │ ├── PasswordGrade.swift │ ├── PreferencesManager.swift │ ├── ProcessHelper.swift │ ├── ReadWriteLock.swift │ ├── ServerLaunch.swift │ ├── StorageManager.swift │ ├── SystemHelper.swift │ └── UpdateHelper.swift ├── Info.plist ├── Library │ └── OpenSSL │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── module.modulemap │ │ └── openssl.h ├── LocalizeStoryboard.swift ├── MessageInterop │ ├── MessageError.swift │ └── MessageManager.swift ├── ObjC │ ├── ObjC.h │ └── ObjC.m ├── Packer │ ├── AirPacker.swift │ ├── BytePacker.swift │ └── PackingError.swift ├── Secrets.default.xcconfig ├── Security │ ├── CertificateTrust.swift │ ├── Certificates │ │ ├── DigiCertGlobalRootCA.crt │ │ └── isrg-root-x1-cross-signed.der │ ├── ForwardCompatURLSessionDelegate.swift │ └── URLSessionCompat.swift ├── SoftwareUpdate.sh ├── Views │ ├── ClientTableCellView.swift │ └── DraggableAppView.swift ├── en.lproj │ ├── Localizable.strings │ └── Localizable.stringsdict ├── fr.lproj │ ├── Localizable.strings │ └── Localizable.stringsdict └── ja.lproj │ ├── Localizable.strings │ └── Localizable.stringsdict ├── AirMessageTests ├── CompressionHelperTests.swift └── ContentTypeHelperTests.swift ├── LICENSE ├── OpenSSL ├── .gitignore ├── Configure.command ├── OpenSSL.h ├── References.txt └── module.modulemap ├── README.md ├── README └── overview.png └── Zlib ├── .gitignore ├── Package.swift └── Sources └── Zlib ├── module.modulemap └── shim.h /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swiftpackagemanager 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,xcode,appcode,swiftpackagemanager 4 | 5 | ### AppCode ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # AWS User-specific 17 | .idea/**/aws.xml 18 | 19 | # Generated files 20 | .idea/**/contentModel.xml 21 | 22 | # Sensitive or high-churn files 23 | .idea/**/dataSources/ 24 | .idea/**/dataSources.ids 25 | .idea/**/dataSources.local.xml 26 | .idea/**/sqlDataSources.xml 27 | .idea/**/dynamic.xml 28 | .idea/**/uiDesigner.xml 29 | .idea/**/dbnavigator.xml 30 | 31 | # Gradle 32 | .idea/**/gradle.xml 33 | .idea/**/libraries 34 | 35 | # Gradle and Maven with auto-import 36 | # When using Gradle or Maven with auto-import, you should exclude module files, 37 | # since they will be recreated, and may cause churn. Uncomment if using 38 | # auto-import. 39 | # .idea/artifacts 40 | # .idea/compiler.xml 41 | # .idea/jarRepositories.xml 42 | # .idea/modules.xml 43 | # .idea/*.iml 44 | # .idea/modules 45 | # *.iml 46 | # *.ipr 47 | 48 | # CMake 49 | cmake-build-*/ 50 | 51 | # Mongo Explorer plugin 52 | .idea/**/mongoSettings.xml 53 | 54 | # File-based project format 55 | *.iws 56 | 57 | # IntelliJ 58 | out/ 59 | 60 | # mpeltonen/sbt-idea plugin 61 | .idea_modules/ 62 | 63 | # JIRA plugin 64 | atlassian-ide-plugin.xml 65 | 66 | # Cursive Clojure plugin 67 | .idea/replstate.xml 68 | 69 | # Crashlytics plugin (for Android Studio and IntelliJ) 70 | com_crashlytics_export_strings.xml 71 | crashlytics.properties 72 | crashlytics-build.properties 73 | fabric.properties 74 | 75 | # Editor-based Rest Client 76 | .idea/httpRequests 77 | 78 | # Android studio 3.1+ serialized cache file 79 | .idea/caches/build_file_checksums.ser 80 | 81 | ### AppCode Patch ### 82 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 83 | 84 | # *.iml 85 | # modules.xml 86 | # .idea/misc.xml 87 | # *.ipr 88 | 89 | # Sonarlint plugin 90 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 91 | .idea/**/sonarlint/ 92 | 93 | # SonarQube Plugin 94 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 95 | .idea/**/sonarIssues.xml 96 | 97 | # Markdown Navigator plugin 98 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 99 | .idea/**/markdown-navigator.xml 100 | .idea/**/markdown-navigator-enh.xml 101 | .idea/**/markdown-navigator/ 102 | 103 | # Cache file creation bug 104 | # See https://youtrack.jetbrains.com/issue/JBR-2257 105 | .idea/$CACHE_FILE$ 106 | 107 | # CodeStream plugin 108 | # https://plugins.jetbrains.com/plugin/12206-codestream 109 | .idea/codestream.xml 110 | 111 | ### macOS ### 112 | # General 113 | .DS_Store 114 | .AppleDouble 115 | .LSOverride 116 | 117 | # Icon must end with two \r 118 | Icon 119 | 120 | 121 | # Thumbnails 122 | ._* 123 | 124 | # Files that might appear in the root of a volume 125 | .DocumentRevisions-V100 126 | .fseventsd 127 | .Spotlight-V100 128 | .TemporaryItems 129 | .Trashes 130 | .VolumeIcon.icns 131 | .com.apple.timemachine.donotpresent 132 | 133 | # Directories potentially created on remote AFP share 134 | .AppleDB 135 | .AppleDesktop 136 | Network Trash Folder 137 | Temporary Items 138 | .apdisk 139 | 140 | ### SwiftPackageManager ### 141 | Packages 142 | .build/ 143 | xcuserdata 144 | DerivedData/ 145 | *.xcodeproj 146 | 147 | 148 | ### Xcode ### 149 | # Xcode 150 | # 151 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 152 | 153 | ## User settings 154 | xcuserdata/ 155 | 156 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 157 | *.xcscmblueprint 158 | *.xccheckout 159 | 160 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 161 | build/ 162 | *.moved-aside 163 | *.pbxuser 164 | !default.pbxuser 165 | *.mode1v3 166 | !default.mode1v3 167 | *.mode2v3 168 | !default.mode2v3 169 | *.perspectivev3 170 | !default.perspectivev3 171 | 172 | ## Gcc Patch 173 | /*.gcno 174 | 175 | ### Xcode Patch ### 176 | *.xcodeproj/* 177 | !*.xcodeproj/project.pbxproj 178 | !*.xcodeproj/xcshareddata/ 179 | !*.xcworkspace/contents.xcworkspacedata 180 | **/xcshareddata/WorkspaceSettings.xcsettings 181 | 182 | # End of https://www.toptal.com/developers/gitignore/api/macos,xcode,appcode,swiftpackagemanager 183 | 184 | AirMessage/Secrets.xcconfig -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | AirMessage -------------------------------------------------------------------------------- /.idea/AirMessage.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sqlite.xerial 6 | true 7 | org.sqlite.JDBC 8 | jdbc:sqlite:$USER_HOME$/Library/Messages/chat.db 9 | $ProjectFileDir$ 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/runConfigurations/AirMessage.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/xcode.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /AirMessage.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AirMessage.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AirMessage.xcodeproj/xcuserdata/cole.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | AirMessage.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 3 11 | 12 | SQLite (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 1 18 | 19 | SQLite (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 2 25 | 26 | SQLite (Playground).xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 0 32 | 33 | 34 | SuppressBuildableAutocreation 35 | 36 | ED28095025A20332009E7636 37 | 38 | primary 39 | 40 | 41 | EDC8985F2752BF290081BDB7 42 | 43 | primary 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /AirMessage/AirMessage-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | #import "ObjC.h" 6 | -------------------------------------------------------------------------------- /AirMessage/AirMessage.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptCodes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppleScriptCodes.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-20. 6 | // 7 | 8 | import Foundation 9 | 10 | class AppleScriptCodes { 11 | static let errorUnauthorized = -1743 //AirMessage doesn't have Automation access 12 | static let errorNoChat = -1728 //The chat isn't available to send a message to 13 | } 14 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/Common/pressCommandReturn.applescript: -------------------------------------------------------------------------------- 1 | tell application "System Events" to keystroke return using command down 2 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/Common/testPermissionsAutomation.applescript: -------------------------------------------------------------------------------- 1 | tell application "System Events" 2 | key code 57 --Shift 3 | end tell 4 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/acceptPendingUser.applescript: -------------------------------------------------------------------------------- 1 | --Accepts the first user waiting for access to the call 2 | 3 | tell application "System Events" 4 | tell process "FaceTime" 5 | repeat with groupEl in groups of list 1 of list 1 of scroll area 2 of window 1 6 | try 7 | if (exists attribute "AXIdentifier" of groupEl) and (value of attribute "AXIdentifier" of groupEl = "InCallControlsPendingParticipantCell") then 8 | repeat while exists button 2 of groupEl 9 | --Accept the user 10 | click button 2 of groupEl 11 | end repeat 12 | return true 13 | end if 14 | end try 15 | end repeat 16 | 17 | return false 18 | end tell 19 | end tell 20 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/centerWindow.applescript: -------------------------------------------------------------------------------- 1 | --Centers the FaceTime window in the middle of the screen 2 | 3 | on main(moveX, moveY) 4 | --Open FaceTime 5 | tell application "FaceTime" to activate 6 | 7 | --Wait for FaceTime to initialize 8 | tell application "System Events" 9 | tell process "FaceTime" 10 | set windowReady to false 11 | repeat while not windowReady 12 | if exists window 1 then 13 | repeat with buttonEl in buttons of window 1 14 | if (exists attribute "AXIdentifier" of buttonEl) and¬ 15 | ((value of attribute "AXIdentifier" of buttonEl contains "NS") or¬ 16 | (value of attribute "AXIdentifier" of buttonEl = "toggleSidebarButton")) then 17 | set windowReady to true 18 | exit repeat 19 | end if 20 | end repeat 21 | end if 22 | delay 0.1 23 | end repeat 24 | end tell 25 | end tell 26 | 27 | --Open FaceTime 28 | tell application "FaceTime" to activate 29 | 30 | --Center the window 31 | tell application "System Events" 32 | tell process "FaceTime" 33 | set {windowWidth, windowHeight} to size of window 1 34 | set position of window 1 to {moveX - (windowWidth / 2), moveY - (windowHeight / 2)} 35 | end tell 36 | end tell 37 | end main 38 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/getActiveLink.applescript: -------------------------------------------------------------------------------- 1 | --Creates and returns a FaceTime link for the current call 2 | 3 | --Open FaceTime 4 | tell application "FaceTime" to activate 5 | 6 | --Wait for FaceTime to initialize 7 | tell application "System Events" 8 | tell process "FaceTime" 9 | set windowReady to false 10 | repeat while not windowReady 11 | if exists window 1 then 12 | set windowReady to true 13 | exit repeat 14 | end if 15 | 16 | delay 0.1 17 | end repeat 18 | end tell 19 | end tell 20 | 21 | tell application "System Events" 22 | tell process "FaceTime" 23 | --Open sidebar 24 | repeat with buttonEl in buttons of window 1 25 | try 26 | if (exists attribute "AXIdentifier" of buttonEl) and (value of attribute "AXIdentifier" of buttonEl = "toggleSidebarButton") then 27 | click buttonEl 28 | end if 29 | end try 30 | end repeat 31 | 32 | --Wait for sidebar to open 33 | delay 1 34 | 35 | --Clear the clipboard 36 | set the clipboard to "" 37 | 38 | # Wait for "share link" button to appear 39 | repeat 40 | if exists of button 2 of last group of list 1 of list 1 of scroll area 2 of window 1 then 41 | --Click "share link" button 42 | set linkButton to button 2 of last group of list 1 of list 1 of scroll area 2 of window 1 43 | click linkButton 44 | delay 0.1 45 | click menu item 1 of menu of linkButton 46 | exit repeat 47 | end if 48 | delay 0.1 49 | end repeat 50 | 51 | set startTime to (current date) 52 | repeat 53 | if (the clipboard) is not "" then 54 | return the clipboard as string 55 | else if (current date) - startTime > 20 then 56 | error "Clipboard timed out" 57 | end if 58 | delay 0.1 59 | end repeat 60 | end tell 61 | end tell 62 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/getNewLink.applescript: -------------------------------------------------------------------------------- 1 | --Creates a new FaceTime link and returns it 2 | 3 | --Open FaceTime 4 | tell application "FaceTime" to activate 5 | 6 | --Wait for FaceTime to initialize 7 | tell application "System Events" 8 | tell process "FaceTime" 9 | set windowReady to false 10 | 11 | repeat while not windowReady 12 | if exists window 1 then 13 | repeat with buttonEl in buttons of window 1 14 | try 15 | if (exists attribute "AXIdentifier" of buttonEl) and (value of attribute "AXIdentifier" of buttonEl contains "NS") then 16 | set windowReady to true 17 | exit repeat 18 | end if 19 | end try 20 | end repeat 21 | end if 22 | 23 | delay 0.1 24 | end repeat 25 | 26 | delay 0.2 27 | end tell 28 | end tell 29 | 30 | tell application "System Events" 31 | tell process "FaceTime" 32 | --Clear the clipboard 33 | set the clipboard to "" 34 | 35 | --Click "Create Link" button 36 | set linkButton to button 1 of window 1 37 | click linkButton 38 | delay 0.1 39 | click menu item 1 of menu of linkButton 40 | 41 | set startTime to (current date) 42 | repeat 43 | if (the clipboard) is not "" then 44 | return the clipboard as string 45 | else if (current date) - startTime > 20 then 46 | error "Clipboard timed out" 47 | end if 48 | delay 0.1 49 | end repeat 50 | end tell 51 | end tell 52 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/handleIncomingCall.applescript: -------------------------------------------------------------------------------- 1 | --Accepts or rejects a pending incoming call 2 | on main(accept) 3 | tell application "System Events" 4 | --Make sure the notification exists 5 | if not (exists group 1 of UI element 1 of scroll area 1 of window 1 of application process "NotificationCenter") then 6 | return false 7 | end if 8 | 9 | --Get the first notification 10 | set notificationGroup to group 1 of UI element 1 of scroll area 1 of window 1 of application process "NotificationCenter" 11 | 12 | --Handle the call 13 | if accept then 14 | set buttonAccept to button 1 of notificationGroup 15 | click buttonAccept 16 | else 17 | set buttonReject to button 2 of notificationGroup 18 | click buttonReject 19 | end if 20 | 21 | return true 22 | end tell 23 | end main 24 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/initiateOutgoingCall.applescript: -------------------------------------------------------------------------------- 1 | on main(addressList) 2 | set labelsMessages to {"Messages", "メッセージ"} 3 | 4 | --Open FaceTime 5 | tell application "FaceTime" to activate 6 | 7 | --Wait for FaceTime to initialize 8 | tell application "System Events" 9 | tell process "FaceTime" 10 | set windowReady to false 11 | repeat while not windowReady 12 | if exists window 1 then 13 | repeat with buttonEl in buttons of window 1 14 | try 15 | if (exists attribute "AXIdentifier" of buttonEl) and (value of attribute "AXIdentifier" of buttonEl contains "NS") then 16 | set windowReady to true 17 | exit repeat 18 | end if 19 | end try 20 | end repeat 21 | end if 22 | delay 0.1 23 | end repeat 24 | end tell 25 | end tell 26 | 27 | tell application "System Events" 28 | tell process "FaceTime" 29 | --Click the "New FaceTime" button 30 | set createButton to button 2 of window 1 31 | click createButton 32 | 33 | --Get the sheet 34 | set createSheet to sheet 1 of window 1 35 | 36 | --Focus the input field 37 | set inputField to text field 1 of createSheet 38 | set focused of inputField to true 39 | 40 | --Enter the addresses 41 | repeat with address in addressList 42 | keystroke address 43 | keystroke return 44 | end repeat 45 | 46 | --Wait for the request to go through 47 | repeat 48 | if (exists of radio group 1 of createSheet) and (enabled of radio group 1 of createSheet) then 49 | --Click the create button and join the call 50 | set buttonCreate to radio button 1 of radio group 1 of createSheet 51 | 52 | # FaceTime button will disable while it queries contacts, so wait for it to enable again, then click it until the sheet disappears 53 | repeat while exists createSheet 54 | if enabled of buttonCreate then 55 | click buttonCreate 56 | end if 57 | delay 0.1 58 | end repeat 59 | 60 | return true 61 | else if exists button 2 of createSheet then 62 | set buttonName to name of button 2 of createSheet 63 | repeat with label in labelsMessages 64 | if buttonName contains label then 65 | --Invite with Messages 66 | --Dismiss the sheet 67 | click button 1 of createSheet 68 | 69 | return false 70 | end if 71 | end repeat 72 | end if 73 | 74 | delay 0.1 75 | end repeat 76 | end tell 77 | end tell 78 | end main 79 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/leaveCall.applescript: -------------------------------------------------------------------------------- 1 | --Leaves a call in progress, or cancels a pending outgoing call 2 | set labelsCancel to {"Cancel", "Annuler", "キャンセル"} 3 | set labelsEnd to {"End", "Raccrocher", "終了"} 4 | 5 | tell application "System Events" 6 | tell process "FaceTime" 7 | --If we're in a call, target the button with an AXIdentifier of "leaveButton" 8 | repeat with buttonEl in buttons of window 1 9 | try 10 | if (exists attribute "AXIdentifier" of buttonEl) and (value of attribute "AXIdentifier" of buttonEl = "leaveButton") then 11 | click buttonEl 12 | return 13 | end if 14 | end try 15 | end repeat 16 | 17 | --If we're trying to make an outgoing call, target the "cancel" or "end" button 18 | set targetButton to button 2 of window 1 19 | set buttonName to name of targetButton 20 | 21 | --The label is "cancel" if the user rejected the call 22 | if labelsCancel contains buttonName then 23 | click targetButton 24 | return 25 | --The label is "end" if we're waiting for a pending outgoing call 26 | else if labelsEnd contains buttonName then 27 | click targetButton 28 | return 29 | end if 30 | end tell 31 | end tell 32 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/queryIncomingCall.applescript: -------------------------------------------------------------------------------- 1 | --Checks Notification Center for incoming calls, and returns the caller's name 2 | tell application "System Events" 3 | --Wait for a notification call to appear 4 | if not (exists group 1 of UI element 1 of scroll area 1 of window 1 of application process "NotificationCenter") then 5 | return "" 6 | end if 7 | 8 | --Get the first notification 9 | set notificationGroup to group 1 of UI element 1 of scroll area 1 of window 1 of application process "NotificationCenter" 10 | 11 | --Make sure we're dealing with a FaceTime call notification 12 | if (exists static text 1 of notificationGroup) and (value of static text 1 of notificationGroup contains "FaceTime Video") then 13 | --Return the name of the caller 14 | set callerName to value of static text 2 of notificationGroup 15 | return callerName 16 | else 17 | return "" 18 | end if 19 | end tell 20 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/FaceTime/queryOutgoingCall.applescript: -------------------------------------------------------------------------------- 1 | --Given an active outgoing call, returns when / whether the user accepted or rejected the call 2 | set labelsCancel to {"Cancel", "Annuler", "キャンセル"} 3 | set labelsEnd to {"End", "Raccrocher", "終了"} 4 | 5 | tell application "System Events" 6 | tell process "FaceTime" 7 | set targetButton to button 2 of window 1 8 | set buttonName to name of targetButton 9 | --The label is "cancel" if the user rejected the call 10 | if labelsCancel contains buttonName then 11 | click targetButton --Exit out of the call 12 | return "rejected" 13 | else if labelsEnd does not contain buttonName then 14 | return "accepted" 15 | else 16 | return "pending" 17 | end if 18 | end tell 19 | end tell 20 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/Messages/createChat.applescript: -------------------------------------------------------------------------------- 1 | --Only works on macOS 10.15 or below 2 | on main(addressList, serviceType) 3 | tell application "Messages" 4 | --Get the service 5 | if serviceType is "iMessage" then 6 | set targetService to 1st service whose service type = iMessage 7 | else 8 | set targetService to service serviceType 9 | end if 10 | 11 | --Create the participants 12 | set participantList to {} 13 | repeat with address in addressList 14 | set end of participantList to a reference to buddy address of targetService 15 | end repeat 16 | 17 | --Create the chat 18 | set createdChat to make new text chat with properties {participants: participantList} 19 | 20 | return createdChat 21 | end tell 22 | end run 23 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/Messages/sendMessageDirect.applescript: -------------------------------------------------------------------------------- 1 | --Sends a message directly to a single recipient, works on macOS 11.0+ 2 | on main(address, serviceType, message, isFile) 3 | if isFile then 4 | set message to POSIX file message 5 | end if 6 | 7 | tell application "Messages" 8 | --Get the service 9 | if serviceType is "iMessage" then 10 | set targetService to 1st service whose service type = iMessage 11 | else 12 | set targetService to service serviceType 13 | end if 14 | 15 | --Get the participant 16 | set targetParticipant to participant address of targetService 17 | 18 | --Send the message 19 | send message to targetParticipant 20 | end tell 21 | end main 22 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/Messages/sendMessageExisting.applescript: -------------------------------------------------------------------------------- 1 | on main(chatID, message, isFile) 2 | if isFile then 3 | set message to POSIX file message 4 | end if 5 | 6 | tell application "Messages" 7 | send message to chat id chatID 8 | end tell 9 | end main 10 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/Messages/sendMessageNew.applescript: -------------------------------------------------------------------------------- 1 | --Only works on macOS 10.15 or below 2 | on main(addressList, serviceType, message, isFile) 3 | if isFile then 4 | set message to POSIX file message 5 | end if 6 | 7 | tell application "Messages" 8 | --Get the service 9 | if serviceType is "iMessage" then 10 | set targetService to 1st service whose service type = iMessage 11 | else 12 | set targetService to service serviceType 13 | end if 14 | 15 | --Create the participants 16 | set participantList to {} 17 | repeat with address in addressList 18 | set end of participantList to a reference to buddy address of targetService 19 | end repeat 20 | 21 | --Create the chat 22 | set createdChat to make new text chat with properties {participants: participantList} 23 | 24 | --Send the message 25 | send message to createdChat 26 | end tell 27 | end main 28 | -------------------------------------------------------------------------------- /AirMessage/AppleScript/AppleScriptSource/Messages/testPermissionsMessages.applescript: -------------------------------------------------------------------------------- 1 | tell application "Messages" 2 | count windows 3 | end tell 4 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AccessibilityAccess-13.imageset/AccessibilityAccess-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AccessibilityAccess-13.imageset/AccessibilityAccess-13.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AccessibilityAccess-13.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AccessibilityAccess-13.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AccessibilityAccess.imageset/AccessibilityAccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AccessibilityAccess.imageset/AccessibilityAccess.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AccessibilityAccess.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AccessibilityAccess.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/Android.imageset/Android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/Android.imageset/Android.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/Android.imageset/Android2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/Android.imageset/Android2.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/Android.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Android.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Android2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@1024Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@1024Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@128Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@128Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@16Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@16Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@256Xcode asset-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@256Xcode asset-1.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@256Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@256Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@32Xcode asset-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@32Xcode asset-1.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@32Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@32Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@512Xcode asset-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@512Xcode asset-1.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@512Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@512Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/@64Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIcon.appiconset/@64Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "@16Xcode asset.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "@32Xcode asset-1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "@32Xcode asset.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "@64Xcode asset.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "@128Xcode asset.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "@256Xcode asset-1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "@256Xcode asset.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "@512Xcode asset-1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "@512Xcode asset.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "@1024Xcode asset.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIconResource.imageset/@1024Xcode asset-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIconResource.imageset/@1024Xcode asset-1.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIconResource.imageset/@256Xcode asset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIconResource.imageset/@256Xcode asset.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIconResource.imageset/@512Xcode asset-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AppIconResource.imageset/@512Xcode asset-1.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AppIconResource.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "@256Xcode asset.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "@512Xcode asset-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "@1024Xcode asset-1.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AutomationAccess-13.imageset/AutomationAccess-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AutomationAccess-13.imageset/AutomationAccess-13.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AutomationAccess-13.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AutomationAccess-13.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AutomationAccess.imageset/AutomationAccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/AutomationAccess.imageset/AutomationAccess.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/AutomationAccess.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AutomationAccess.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserChrome.imageset/BrowserChrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserChrome.imageset/BrowserChrome.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserChrome.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BrowserChrome.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "diagram.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserChrome.imageset/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserChrome.imageset/diagram.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserEdge.imageset/BrowserEdge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserEdge.imageset/BrowserEdge.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserEdge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BrowserEdge.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Edge2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserEdge.imageset/Edge2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserEdge.imageset/Edge2.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserFirefox.imageset/BrowserFirefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserFirefox.imageset/BrowserFirefox.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserFirefox.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BrowserFirefox.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "diagram.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserFirefox.imageset/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserFirefox.imageset/diagram.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserOpera.imageset/BrowserOpera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserOpera.imageset/BrowserOpera.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserOpera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BrowserOpera.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "diagram.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserOpera.imageset/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserOpera.imageset/diagram.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserSafari.imageset/BrowserSafari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserSafari.imageset/BrowserSafari.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserSafari.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BrowserSafari.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "diagram.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserSafari.imageset/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserSafari.imageset/diagram.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserSamsung.imageset/BrowserSamsung.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserSamsung.imageset/BrowserSamsung.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserSamsung.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "BrowserSamsung.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "samsung.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/BrowserSamsung.imageset/samsung.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/BrowserSamsung.imageset/samsung.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/FullDiskAccess-13.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FullDiskAccess-13.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/FullDiskAccess-13.imageset/FullDiskAccess-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/FullDiskAccess-13.imageset/FullDiskAccess-13.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/FullDiskAccess.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "FullDiskAccess.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/FullDiskAccess.imageset/FullDiskAccess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/FullDiskAccess.imageset/FullDiskAccess.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/StatusBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "StatusBarIcon.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/Windows.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Windows.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Windows2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/Windows.imageset/Windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/Windows.imageset/Windows.png -------------------------------------------------------------------------------- /AirMessage/Assets.xcassets/Windows.imageset/Windows2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Assets.xcassets/Windows.imageset/Windows2.png -------------------------------------------------------------------------------- /AirMessage/Compat/FileHandleCompat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileHandleCompat.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-16. 6 | // 7 | 8 | import Foundation 9 | 10 | extension FileHandle { 11 | func readCompat(upToCount count: Int) throws -> Data { 12 | if #available(macOS 10.15.4, *) { 13 | return try read(upToCount: count) ?? Data() 14 | } else { 15 | //FileHandle.readData(ofLength:) raises an NSException if the read fails 16 | var data: Data? = nil 17 | try ObjC.catchException { 18 | data = readData(ofLength: count) 19 | } 20 | return data! 21 | } 22 | } 23 | 24 | func writeCompat(contentsOf data: Data) throws { 25 | var swiftError: Error? = nil 26 | 27 | try ObjC.catchException { 28 | if #available(macOS 10.15.4, *) { 29 | //Absorb NSError, since ObjC.catchException can't handle it 30 | do { 31 | try write(contentsOf: data) 32 | } catch { 33 | swiftError = error 34 | } 35 | } else { 36 | //FileHandle.write(_:) raises an NSException if the write fails 37 | write(data) 38 | } 39 | } 40 | 41 | //Throw error if needed 42 | if let swiftError = swiftError { 43 | throw swiftError 44 | } 45 | } 46 | 47 | func readToEndCompat() throws -> Data { 48 | if #available(macOS 10.15.4, *) { 49 | return try readToEnd() ?? Data() 50 | } else { 51 | //FileHandle.readDataToEndOfFile() raises an NSException if the read fails 52 | var data: Data? = nil 53 | try ObjC.catchException { 54 | data = readDataToEndOfFile() 55 | } 56 | return data! 57 | } 58 | } 59 | 60 | func closeCompat() throws { 61 | if #available(macOS 10.15, *) { 62 | try close() 63 | } else { 64 | closeFile() 65 | } 66 | } 67 | 68 | func seekToEndCompat() throws { 69 | if #available(macOS 10.15.4, *) { 70 | try seekToEnd() 71 | } else { 72 | //FileHandle.seekToEndOfFile() raises an NSException if the write fails 73 | try ObjC.catchException { 74 | seekToEndOfFile() 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /AirMessage/Connection/AdvancedAttachmentsFilter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdvancedAttachmentsFilter.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AdvancedAttachmentsFilter { 11 | let timeSince: Int64? 12 | let maxSize: Int64? 13 | 14 | let whitelist: [String] //If it's on the whitelist, download it 15 | let blacklist: [String] //If it's on the blacklist, skip it 16 | let downloadExceptions: Bool //If it's on neither list, download if if this value is true 17 | 18 | /** 19 | Checks if the attachment passes this filter 20 | */ 21 | func apply(to attachment: AttachmentInfo, ofDate: Int64) -> Bool { 22 | //Check time since 23 | if let timeSince = timeSince, 24 | ofDate < timeSince { 25 | return false 26 | } 27 | 28 | //Check max size 29 | if let maxSize = maxSize, attachment.size > maxSize { 30 | return false 31 | } 32 | 33 | //Check content type 34 | guard let attachmentType = attachment.type else { 35 | return false 36 | } 37 | 38 | if whitelist.contains(where: { type in compareMIMETypes(type, attachmentType) }) { 39 | return true 40 | } 41 | 42 | if blacklist.contains(where: { type in compareMIMETypes(type, attachmentType) }) { 43 | return false 44 | } 45 | 46 | return downloadExceptions 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /AirMessage/Connection/ClientConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-13. 3 | // 4 | 5 | import Foundation 6 | 7 | class ClientConnection { 8 | let id: Int32 9 | 10 | //Overridable by subclasses 11 | var readableID: String { String(id) } 12 | 13 | struct Registration { 14 | /** 15 | * The installation ID of this instance 16 | * Used for blocking multiple connections from the same client 17 | */ 18 | let installationID: String 19 | 20 | /** 21 | * A human-readable name for this client 22 | * Used when displaying connected clients to the user 23 | * Examples: 24 | * - Samsung Galaxy S20 25 | * - Firefox 75 26 | */ 27 | let clientName: String 28 | 29 | /** 30 | * The ID of the platform this device is running on 31 | * Examples: 32 | * - "android" (AirMessage for Android) 33 | * - "google chrome" (AirMessage for web) 34 | * - "windows" (AirMessage for Windows) 35 | */ 36 | let platformID: String 37 | } 38 | //Registration information for this client, once it's completed its handshake with the server 39 | var registration: Registration? 40 | 41 | //The current awaited transmission check for this client 42 | var transmissionCheck: Data? 43 | 44 | //Whether this client is connected. Set to false when the client disconnects. 45 | var isConnected = AtomicBool(initialValue: true) 46 | 47 | init(id: Int32) { 48 | self.id = id 49 | } 50 | 51 | deinit { 52 | //Ensure timers are cleaned up 53 | assert(timerDict.isEmpty, "Client connection was deinitialized with active timers") 54 | } 55 | 56 | //MARK: Timers 57 | 58 | enum TimerType { 59 | case handshakeExpiry 60 | case pingExpiry 61 | } 62 | 63 | private var timerDict: [TimerType: DispatchSourceTimer] = [:] 64 | 65 | /** 66 | Cancels all pending expiry timers 67 | */ 68 | func cancelAllTimers() { 69 | //Run on the timer queue 70 | runOnQueue(queue: CustomQueue.timerQueue, key: CustomQueue.timerQueueKey) { 71 | for (_, runningTimer) in timerDict { 72 | runningTimer.cancel() 73 | } 74 | timerDict.removeAll() 75 | } 76 | } 77 | 78 | /** 79 | Cancels the expiry timer of the specified type 80 | */ 81 | func cancelTimer(ofType type: TimerType) { 82 | //Run on the timer queue 83 | runOnQueue(queue: CustomQueue.timerQueue, key: CustomQueue.timerQueueKey) { 84 | timerDict[type]?.cancel() 85 | timerDict[type] = nil 86 | } 87 | } 88 | 89 | /** 90 | Schedules a timer of the specified type after interval to run the callback 91 | If a timer was previously scheduled of this type, that timer is cancelled and replaced with this one 92 | */ 93 | func startTimer(ofType type: TimerType, interval: TimeInterval, callback: @escaping (ClientConnection) -> Void) { 94 | //Run on the timer queue 95 | runOnQueue(queue: CustomQueue.timerQueue, key: CustomQueue.timerQueueKey) { 96 | //Cancel existing timers 97 | cancelTimer(ofType: type) 98 | 99 | //Create and start the timer 100 | let timer = DispatchSource.makeTimerSource(queue: CustomQueue.timerQueue) 101 | timer.schedule(deadline: .now() + interval, repeating: .never) 102 | timer.setEventHandler { [weak self] in 103 | //Make sure we're still on the same queue 104 | assertDispatchQueue(CustomQueue.timerQueue) 105 | 106 | //Check our reference to self 107 | guard let self = self else { 108 | return 109 | } 110 | 111 | //Invoke the callback 112 | callback(self) 113 | 114 | //Remove this timer 115 | self.timerDict[type] = nil 116 | } 117 | timer.resume() 118 | timerDict[type] = timer 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /AirMessage/Connection/CommConst.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-13. 3 | // 4 | 5 | import Foundation 6 | 7 | class CommConst { 8 | static let version: Int32 = 5 9 | static let subVersion: Int32 = 5 10 | 11 | static let defaultFileChunkSize: Int64 = 1024 * 1024 //1 MB 12 | 13 | //Timeouts 14 | static let handshakeTimeout: TimeInterval = 10 //10 seconds 15 | static let pingTimeout: TimeInterval = 30 //30 seconds 16 | static let keepAliveInterval: TimeInterval = 30 * 60 //30 minutes 17 | 18 | static let maxPacketAllocation = 50 * 1024 * 1024 //50 MB 19 | 20 | static let transmissionCheckLength = 32 21 | } 22 | 23 | //Net header type 24 | enum NHT: Int32 { 25 | case close = 0 26 | case ping = 1 27 | case pong = 2 28 | 29 | case information = 100 30 | case authentication = 101 31 | 32 | case messageUpdate = 200 33 | case timeRetrieval = 201 34 | case idRetrieval = 202 35 | case massRetrieval = 203 36 | case massRetrievalFile = 204 37 | case massRetrievalFinish = 205 38 | case conversationUpdate = 206 39 | case modifierUpdate = 207 40 | case attachmentReq = 208 41 | case attachmentReqConfirm = 209 42 | case attachmentReqFail = 210 43 | case idUpdate = 211 44 | 45 | case liteConversationRetrieval = 300 46 | case liteThreadRetrieval = 301 47 | 48 | case sendResult = 400 49 | case sendTextExisting = 401 50 | case sendTextNew = 402 51 | case sendFileExisting = 403 52 | case sendFileNew = 404 53 | case createChat = 405 54 | 55 | case softwareUpdateListing = 500 56 | case softwareUpdateInstall = 501 57 | case softwareUpdateError = 502 58 | 59 | case faceTimeCreateLink = 600 //Create a new FaceTime link 60 | case faceTimeOutgoingInitiate = 601 //Initiate a new FaceTime call 61 | case faceTimeOutgoingHandled = 602 //Notify a client that an outgoing call has been accepted or rejected 62 | case faceTimeIncomingCallerUpdate = 603 //Notify clients that there is a new incoming call 63 | case faceTimeIncomingHandle = 604 //Client -> Server: accept or reject the incoming call and return its link 64 | case faceTimeDisconnect = 605 //Client -> Server: Drop the current call 65 | } 66 | 67 | //Net sub-type 68 | enum NSTAuth: Int32 { 69 | case ok = 0 70 | case unauthorized = 1 71 | case badRequest = 2 72 | } 73 | 74 | enum NSTSendResult: Int32 { 75 | case ok = 0 76 | case scriptError = 1; //Some unknown AppleScript error 77 | case badRequest = 2 //Invalid data received 78 | case unauthorized = 3 //System rejected request to send message 79 | case noConversation = 4 //A valid conversation wasn't found 80 | case requestTimeout = 5 //File data blocks stopped being received 81 | case internalError = 6 //An internal error occurred 82 | } 83 | 84 | enum NSTAttachmentRequest: Int32 { 85 | case notFound = 1 //File GUID not found 86 | case notSaved = 2 //File (on disk) not found 87 | case unreadable = 3 //No access to file 88 | case io = 4 //IO error 89 | } 90 | 91 | enum NSTCreateChat: Int32 { 92 | case ok = 0 93 | case scriptError = 1 //Some unknown AppleScript error 94 | case badRequest = 2 //Invalid data received 95 | case unauthorized = 3 //System rejected request to send message 96 | case notSupported = 4 //Operation not supported by this server 97 | } 98 | 99 | enum NSTInitiateFaceTimeCall: Int32 { 100 | case ok = 0 101 | case badMembers = 1 102 | case appleScriptError = 2 103 | } 104 | 105 | enum NSTOutgoingFaceTimeCallHandled: Int32 { 106 | case accepted = 0 107 | case rejected = 1 108 | case error = 2 109 | } 110 | 111 | enum PushNotificationPayloadType: Int32 { 112 | case message = 0 113 | case faceTime = 1 114 | } 115 | -------------------------------------------------------------------------------- /AirMessage/Connection/Connect/ConnectConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectConstants.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-23. 6 | // 7 | 8 | import Foundation 9 | 10 | class ConnectConstants { 11 | //AirMessage Connect communications version 12 | static let commVer = 1 13 | 14 | //Timeout for handshake from server 15 | static let handshakeTimeout: TimeInterval = 15 16 | } 17 | 18 | enum ConnectNHT: Int32 { 19 | //Shared Net header types 20 | /* 21 | * The connected device has been connected successfully 22 | */ 23 | case connectionOK = 0 24 | 25 | //Client-only net header types 26 | 27 | /* 28 | * Proxy the message to the server (client -> connect) 29 | * 30 | * payload - data 31 | */ 32 | case clientProxy = 100 33 | 34 | /* 35 | * Add an item to the list of FCM tokens (client -> connect) 36 | * 37 | * string - registration token 38 | */ 39 | case clientAddFCMToken = 110 40 | 41 | /* 42 | * Remove an item from the list of FCM tokens (client -> connect) 43 | * 44 | * string - registration token 45 | */ 46 | case clientRemoveFCMToken = 111 47 | 48 | //Server-only net header types 49 | 50 | /* 51 | * Notify a new client connection (connect -> server) 52 | * 53 | * int - connection ID 54 | */ 55 | case serverOpen = 200 56 | 57 | /* 58 | * Close a connected client (server -> connect) 59 | * Notify a closed connection (connect -> server) 60 | * 61 | * int - connection ID 62 | */ 63 | case serverClose = 201 64 | 65 | /* 66 | * Proxy the message to the client (server -> connect) 67 | * Receive data from a connected client (connect -> server) 68 | * 69 | * int - connection ID 70 | * payload - data 71 | */ 72 | case serverProxy = 210 73 | 74 | /* 75 | * Proxy the message to all connected clients (server -> connect) 76 | * 77 | * payload - data 78 | */ 79 | case serverProxyBroadcast = 211 80 | 81 | /* 82 | * Notify offline clients of a new message 83 | */ 84 | case serverNotifyPush = 212 85 | } 86 | 87 | enum ConnectCloseCode: UInt16 { 88 | case incompatibleProtocol = 4000 //No protocol version matching the one requested 89 | case noGroup = 4001 //There is no active group with a matching ID 90 | case noCapacity = 4002 //The client's group is at capacity 91 | case accountValidation = 4003 //This account couldn't be validated 92 | case serverTokenRefresh = 4004 //The server's provided installation ID is out of date; log in again to re-link this device 93 | case noActivation = 4005 //This user's account is not activated 94 | case otherLocation = 4006 //Logged in from another location 95 | } 96 | -------------------------------------------------------------------------------- /AirMessage/Connection/DataProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-13. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol DataProxy: AnyObject { 8 | /** 9 | Sets the delegate of this proxy for updates 10 | */ 11 | var delegate: DataProxyDelegate? { get set } 12 | 13 | /** 14 | Gets the non-localized name of this proxy 15 | */ 16 | var name: String { get } 17 | 18 | /** 19 | Gets whether this protocol requires the server to actively maintain connections 20 | */ 21 | var requiresPersistence: Bool { get } 22 | 23 | /** 24 | Gets whether this protocol supports push notifications 25 | */ 26 | var supportsPushNotifications: Bool { get } 27 | 28 | /** 29 | Gets a list of connected clients 30 | */ 31 | var connections: [ClientConnection] { get } 32 | var connectionsLock: ReadWriteLock { get } 33 | 34 | /** 35 | Starts this server, allowing it to accept incoming connections 36 | */ 37 | func startServer() 38 | 39 | /** 40 | Stops the server, disconnecting all connected clients 41 | */ 42 | func stopServer() 43 | 44 | /** 45 | Sends a message to the specified client 46 | - Parameters: 47 | - data: The data to send 48 | - client: The client to send the data to, or nil to broadcast 49 | - encrypt: Whether or not to encrypt this data 50 | - onSent: A callback invoked when the message is sent, or nil to ignore 51 | */ 52 | func send(message data: Data, to client: ClientConnection?, encrypt: Bool, onSent: (() -> Void)?) 53 | 54 | /** 55 | Sends a push notification to notify all disconnected clients of new information 56 | - Parameters: 57 | - data: The data to send 58 | - version: The version number to attach to this data 59 | */ 60 | func send(pushNotification data: Data, version: Int) 61 | 62 | /** 63 | Disconnects a client from this server 64 | */ 65 | func disconnect(client: ClientConnection) 66 | } 67 | -------------------------------------------------------------------------------- /AirMessage/Connection/DataProxyDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-13. 3 | // 4 | 5 | import Foundation 6 | 7 | protocol DataProxyDelegate: AnyObject { 8 | typealias C = ClientConnection 9 | 10 | /** 11 | Called when the proxy is started successfully 12 | */ 13 | func dataProxyDidStart(_ dataProxy: DataProxy) 14 | 15 | /** 16 | Called when the proxy is stopped 17 | If isRecoverable is true, the proxy will continue trying to reconnect in the background, 18 | and call `dataProxyDidStart` when the connection is resolved. 19 | */ 20 | func dataProxy(_ dataProxy: DataProxy, didStopWithState state: ServerState, isRecoverable: Bool) 21 | 22 | /** 23 | Called when a new client is connected 24 | */ 25 | func dataProxy(_ dataProxy: DataProxy, didConnectClient client: C, totalCount: Int) 26 | 27 | /** 28 | Called when a client is disconnected 29 | */ 30 | func dataProxy(_ dataProxy: DataProxy, didDisconnectClient client: C, totalCount: Int) 31 | 32 | /** 33 | Called when a message is received 34 | - Parameters: 35 | - data: The message data 36 | - client: The client that sent the message 37 | - wasEncrypted: True if this message was encrypted during transit (and probably contains sensitive content) 38 | */ 39 | func dataProxy(_ dataProxy: DataProxy, didReceive data: Data, from client: C, wasEncrypted: Bool) 40 | } 41 | -------------------------------------------------------------------------------- /AirMessage/Connection/Direct/ClientConnectionTCP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-13. 3 | // 4 | 5 | import Foundation 6 | 7 | class ClientConnectionTCP: ClientConnection { 8 | //Constants 9 | private static let headerLen = MemoryLayout.size + MemoryLayout.size 10 | 11 | //Parameters 12 | private let handle: FileHandle 13 | private let address: String 14 | private weak var delegate: ClientConnectionTCPDelegate? 15 | 16 | override var readableID: String { address } 17 | 18 | //State 19 | private var isRunning = AtomicBool(initialValue: false) 20 | 21 | init(id: Int32, handle: FileHandle, address: String, delegate: ClientConnectionTCPDelegate? = nil) { 22 | self.handle = handle 23 | self.delegate = delegate 24 | self.address = address 25 | super.init(id: id) 26 | } 27 | 28 | func start(on queue: DispatchQueue) { 29 | //Return if we're already running 30 | guard !isRunning.with({ value in 31 | //If we're not running, change the property to running 32 | let originalValue = value 33 | if !originalValue { 34 | value = true 35 | } 36 | return originalValue 37 | }) else { return } 38 | 39 | //Start reader task 40 | queue.async { [weak self] in 41 | do { 42 | while self?.isRunning.value ?? false { 43 | guard let self = self else { break } 44 | 45 | //Read packet header 46 | let packetHeader = try ClientConnectionTCP.read(handle: self.handle, exactCount: ClientConnectionTCP.headerLen) 47 | let (contentLen, isEncrypted) = packetHeader.withUnsafeBytes { ptr in 48 | ( 49 | Int32(bigEndian: ptr.load(fromByteOffset: 0, as: Int32.self)), 50 | ptr.load(fromByteOffset: MemoryLayout.size, as: Bool.self) 51 | ) 52 | } 53 | 54 | //Check if the content length is greater than the maximum packet allocation 55 | guard contentLen < CommConst.maxPacketAllocation else { 56 | //Log and disconnect 57 | LogManager.log("Rejecting large packet (size \(contentLen))", level: .notice) 58 | 59 | self.stop() 60 | break 61 | } 62 | 63 | //Read the content 64 | let packetContent = try ClientConnectionTCP.read(handle: self.handle, exactCount: Int(contentLen)) 65 | self.delegate?.clientConnectionTCP(self, didReceive: packetContent, isEncrypted: isEncrypted) 66 | } 67 | } catch { 68 | //Log and disconnect 69 | LogManager.log("An error occurred while reading client data: \(error)", level: .notice) 70 | self?.stop() 71 | } 72 | } 73 | } 74 | 75 | func stop() { 76 | //Return if we're not running 77 | guard isRunning.with({ value in 78 | //If we're running, change the property to not running 79 | let originalValue = value 80 | if originalValue { 81 | value = false 82 | } 83 | return originalValue 84 | }) else { return } 85 | 86 | //Update the connected property 87 | isConnected.value = false 88 | 89 | //Close the file handle 90 | do { 91 | try handle.closeCompat() 92 | } catch { 93 | LogManager.log("An error occurred while closing a client handle: \(error)", level: .notice) 94 | } 95 | 96 | //Cancel timers 97 | cancelAllTimers() 98 | 99 | //Log a message 100 | LogManager.log("Client disconnected from \(address)", level: .info) 101 | 102 | //Call the delegate 103 | delegate?.clientConnectionTCPDidInvalidate(self) 104 | } 105 | 106 | func reset() { 107 | //Remove registration 108 | registration = nil 109 | 110 | //Cancel timers 111 | cancelAllTimers() 112 | } 113 | 114 | /** 115 | Writes the provided data to the client 116 | - Parameters: 117 | - data: The data to write 118 | - isEncrypted: Whether to mark the data as encrypted 119 | */ 120 | @discardableResult 121 | func write(data: Data, isEncrypted: Bool) -> Bool { 122 | //Make sure this client is still running 123 | guard isRunning.value else { return false } 124 | 125 | //Create the packet structure 126 | var output = Data(capacity: MemoryLayout.size + MemoryLayout.size + data.count) 127 | withUnsafeBytes(of: Int32(data.count).bigEndian) { output.append(contentsOf: $0) } 128 | output.append(isEncrypted ? 1 : 0) 129 | output.append(data) 130 | 131 | //Write the packet 132 | do { 133 | try handle.writeCompat(contentsOf: output) 134 | } catch { 135 | LogManager.log("An error occurred while writing client data to \(address): \(error.localizedDescription)", level: .notice) 136 | return false 137 | } 138 | 139 | return true 140 | } 141 | 142 | deinit { 143 | //Ensure this connection isn't running when we go out of scope 144 | assert(!isRunning.value, "ClientConnectionTCP was deinitialized while active") 145 | } 146 | 147 | enum ReadError: Error { 148 | case eof 149 | case readError 150 | } 151 | 152 | /** 153 | Reads from a `FileHandle` 154 | - Parameters: 155 | - handle: The handle to read from 156 | - count: The maximum amount of bytes to read 157 | - Returns: The read data 158 | - Throws: A read error if the file handle could not be read 159 | */ 160 | private static func read(handle: FileHandle, upToCount count: Int) throws -> Data { 161 | let data: Data 162 | do { 163 | data = try handle.readCompat(upToCount: count) 164 | } catch { 165 | throw ReadError.readError 166 | } 167 | 168 | if data.isEmpty { 169 | throw ReadError.eof 170 | } 171 | 172 | return data 173 | } 174 | 175 | /** 176 | Reads an exact amount of bytes from a `FileHandle` 177 | - Parameters: 178 | - handle: The handle to read from 179 | - count: The amount of bytes to read 180 | - Returns: The read data 181 | - Throws: A read error if the file handle could not be read 182 | */ 183 | private static func read(handle: FileHandle, exactCount count: Int) throws -> Data { 184 | var exactData = Data(capacity: count) 185 | 186 | repeat { 187 | let data = try read(handle: handle, upToCount: count - exactData.count) 188 | exactData.append(data) 189 | } while exactData.count < count 190 | 191 | return exactData 192 | } 193 | } 194 | 195 | protocol ClientConnectionTCPDelegate: AnyObject { 196 | /** 197 | Tells the delegate that this connection received an incoming message 198 | - Parameters: 199 | - client: The client calling this method 200 | - data: The data received from the client 201 | - isEncrypted: Whether the received data is encrypted 202 | */ 203 | func clientConnectionTCP(_ client: ClientConnectionTCP, didReceive data: Data, isEncrypted: Bool) 204 | 205 | /** 206 | Tells the delegate that this connection encountered an error, and must be closed 207 | - Parameters: 208 | - client: The client calling this method 209 | */ 210 | func clientConnectionTCPDidInvalidate(_ client: ClientConnectionTCP) 211 | } 212 | -------------------------------------------------------------------------------- /AirMessage/Connection/FileDownloadRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileDownloadRequest.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-20. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FileDownloadRequestCreateError: LocalizedError { 11 | let path: String 12 | 13 | var errorDescription: String? { 14 | "Failed to create file at \(path)" 15 | } 16 | } 17 | 18 | class FileDownloadRequest { 19 | private static let timeout: TimeInterval = 10 20 | 21 | let requestID: Int16 22 | let fileName: String 23 | let customData: Any 24 | 25 | private(set) var packetsWritten = 0 26 | 27 | private var timeoutTimer: DispatchSourceTimer? 28 | var timeoutCallback: (() -> Void)? 29 | 30 | private let fileDirURL: URL //The container directory of the file 31 | let fileURL: URL //The file 32 | let fileHandle: FileHandle 33 | 34 | private let decompressPipe: CompressionPipeInflate 35 | 36 | /** 37 | Initializes a new file download request 38 | - Parameters: 39 | - fileName: The name of the file to save 40 | - requestID: A number to represent this request, not used internally 41 | - customData: Any extra data to associate with this request, not used internally 42 | */ 43 | init(fileName: String, requestID: Int16, customData: Any) throws { 44 | self.fileName = fileName 45 | self.requestID = requestID 46 | self.customData = customData 47 | 48 | //Initialize the decompression pipe 49 | decompressPipe = try CompressionPipeInflate() 50 | 51 | //Find a place to store this file 52 | fileDirURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString, isDirectory: true) 53 | try FileManager.default.createDirectory(at: fileDirURL, withIntermediateDirectories: false, attributes: nil) 54 | fileURL = fileDirURL.appendingPathComponent(fileName, isDirectory: false) 55 | guard FileManager.default.createFile(atPath: fileURL.path, contents: nil) else { 56 | throw FileDownloadRequestCreateError(path: fileURL.path) 57 | } 58 | 59 | //Open a file handle to the file 60 | do { 61 | fileHandle = try FileHandle(forWritingTo: fileURL) 62 | } catch { 63 | //Clean up and rethrow 64 | try? FileManager.default.removeItem(at: fileDirURL) 65 | throw error 66 | } 67 | } 68 | 69 | /** 70 | Appends data to this request, decompressing it and writing it to the file 71 | */ 72 | func append(_ data: inout Data) throws { 73 | //Decompress the data 74 | let decompressedData = try decompressPipe.pipe(data: &data) 75 | 76 | //Write the data 77 | try fileHandle.writeCompat(contentsOf: decompressedData) 78 | 79 | //Update the counter 80 | packetsWritten += 1 81 | } 82 | 83 | /** 84 | Gets if this download request has received all the data it needs, and should be completed 85 | */ 86 | var isDataComplete: Bool { decompressPipe.isFinished } 87 | 88 | /** 89 | Starts or resets the timeout timer, which invokes `timeoutCallback` 90 | */ 91 | func startTimeoutTimer() { 92 | CustomQueue.timerQueue.sync { 93 | //Cancel the old timer 94 | timeoutTimer?.cancel() 95 | 96 | //Create the new timer 97 | let timer = DispatchSource.makeTimerSource(queue: CustomQueue.timerQueue) 98 | timer.schedule(deadline: .now() + FileDownloadRequest.timeout, repeating: .never) 99 | timer.setEventHandler(handler: onTimeout) 100 | timer.resume() 101 | timeoutTimer = timer 102 | } 103 | } 104 | 105 | /** 106 | Cancels the current timeout timer 107 | */ 108 | func stopTimeoutTimer() { 109 | CustomQueue.timerQueue.sync { 110 | timeoutTimer?.cancel() 111 | timeoutTimer = nil 112 | } 113 | } 114 | 115 | private func onTimeout() { 116 | timeoutTimer = nil 117 | timeoutCallback?() 118 | } 119 | 120 | func cleanUp() throws { 121 | //Close the file handle 122 | try? fileHandle.closeCompat() 123 | 124 | //Remove the directory 125 | try FileManager.default.removeItem(at: fileDirURL) 126 | } 127 | 128 | deinit { 129 | //Make sure the timer is cancelled 130 | stopTimeoutTimer() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /AirMessage/Constants/AccountType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-07-03. 3 | // 4 | 5 | import Foundation 6 | 7 | @objc enum AccountType: Int { 8 | case unknown = -1 9 | case direct = 0 10 | case connect = 1 11 | } 12 | -------------------------------------------------------------------------------- /AirMessage/Constants/CustomQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomQueue.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2022-06-08. 6 | // 7 | 8 | import Foundation 9 | 10 | class CustomQueue { 11 | static let timerQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".task.timer", qos: .utility) 12 | static let timerQueueKey = DispatchSpecificKey() 13 | 14 | static func register() { 15 | timerQueue.setSpecific(key: timerQueueKey, value: true) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AirMessage/Constants/NotificationNames.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationNames.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-14. 6 | // 7 | 8 | import Foundation 9 | 10 | class NotificationNames { 11 | static let updateServerState = NSNotification.Name("updateServerState") 12 | static let updateServerStateParam = "state" 13 | 14 | static let updateSetupMode = NSNotification.Name("updateSetupMode") 15 | static let updateSetupModeParam = "isSetupMode" 16 | 17 | static let updateConnectionCount = NSNotification.Name("updateConnectionCount") 18 | static let updateConnectionCountParam = "count" 19 | 20 | static let signOut = NSNotification.Name("signOut") 21 | 22 | /** 23 | Posts a UI state update to `NotificationCenter` in a thread-safe manner 24 | */ 25 | static func postUpdateUIState(_ state: ServerState) { 26 | runOnMainAsync { 27 | NotificationCenter.default.post(name: NotificationNames.updateServerState, object: nil, userInfo: [NotificationNames.updateServerStateParam: state.rawValue]) 28 | } 29 | } 30 | 31 | /** 32 | Posts a setup mode update to `NotificationCenter` in a thread-safe manner 33 | */ 34 | static func postUpdateSetupMode(_ setupMode: Bool) { 35 | runOnMainAsync { 36 | NotificationCenter.default.post(name: NotificationNames.updateSetupMode, object: nil, userInfo: [NotificationNames.updateSetupModeParam: setupMode]) 37 | } 38 | } 39 | 40 | /** 41 | Posts a setup mode update to `NotificationCenter` in a thread-safe manner 42 | */ 43 | static func postUpdateConnectionCount(_ connectionCount: Int) { 44 | runOnMainAsync { 45 | NotificationCenter.default.post(name: NotificationNames.updateConnectionCount, object: nil, userInfo: [NotificationNames.updateConnectionCountParam: connectionCount]) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /AirMessage/Constants/ServerState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerState.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-04. 6 | // 7 | 8 | import Foundation 9 | 10 | fileprivate let typeStatuses: [ServerState] = [.setup, .starting, .connecting, .running, .stopped] 11 | 12 | fileprivate let typeRequiresReauth: [ServerState] = [.errorConnValidation, .errorConnToken, .errorConnAccountConflict] 13 | 14 | enum ServerState: Int { 15 | case setup = 1 16 | case starting = 2 17 | case connecting = 3 18 | case running = 4 19 | case stopped = 5 20 | 21 | case errorDatabase = 100 //Couldn't connect to database 22 | case errorInternal = 101 //Internal error 23 | case errorExternal = 102 //External error 24 | case errorInternet = 103 //No internet connection 25 | case errorKeychain = 104 //Keychain error 26 | 27 | case errorTCPPort = 200 //Port unavailable 28 | case errorTCPInternal = 201 //Internal TCP error 29 | 30 | case errorConnBadRequest = 300 //Bad request 31 | case errorConnOutdated = 301 //Client out of date 32 | case errorConnValidation = 302 //Account access not valid 33 | case errorConnToken = 303 //Token refresh 34 | case errorConnActivation = 304 //Not subscribed (not enrolled) 35 | case errorConnAccountConflict = 305 //Logged in from another location 36 | 37 | var description: String { 38 | switch(self) { 39 | case .setup: 40 | return NSLocalizedString("message.status.setup", comment: "") 41 | case .starting: 42 | return NSLocalizedString("message.status.starting", comment: "") 43 | case .connecting: 44 | return NSLocalizedString("message.status.connecting", comment: "") 45 | case .running: 46 | return NSLocalizedString("message.status.running", comment: "") 47 | case .stopped: 48 | return NSLocalizedString("message.status.stopped", comment: "") 49 | case .errorDatabase: 50 | return NSLocalizedString("message.status.error.database", comment: "") 51 | case .errorInternal: 52 | return NSLocalizedString("message.status.error.internal", comment: "") 53 | case .errorExternal: 54 | return NSLocalizedString("message.status.error.external", comment: "") 55 | case .errorInternet: 56 | return NSLocalizedString("message.status.error.internet", comment: "") 57 | case .errorKeychain: 58 | return NSLocalizedString("message.status.error.keychain", comment: "") 59 | case .errorTCPPort: 60 | return NSLocalizedString("message.status.error.port_unavailable", comment: "") 61 | case .errorTCPInternal: 62 | return NSLocalizedString("message.status.error.port_error", comment: "") 63 | case .errorConnBadRequest: 64 | return NSLocalizedString("message.status.error.bad_request", comment: "") 65 | case .errorConnOutdated: 66 | return NSLocalizedString("message.status.error.outdated", comment: "") 67 | case .errorConnValidation: 68 | return NSLocalizedString("message.status.error.account_validation", comment: "") 69 | case .errorConnToken: 70 | return NSLocalizedString("message.status.error.token_refresh", comment: "") 71 | case .errorConnActivation: 72 | return NSLocalizedString("message.status.error.no_activation", comment: "") 73 | case .errorConnAccountConflict: 74 | return NSLocalizedString("message.status.error.account_conflict", comment: "") 75 | } 76 | } 77 | 78 | var isError: Bool { 79 | !typeStatuses.contains(self) 80 | } 81 | 82 | var recoveryType: ServerStateRecovery { 83 | if typeRequiresReauth.contains(self) { 84 | return .reauthenticate 85 | } else { 86 | return .retry 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /AirMessage/Constants/ServerStateRecovery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerStateRecovery.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-14. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ServerStateRecovery { 11 | case none //This error cannot be recovered by the user 12 | case retry //The user can retry by clicking a button 13 | case reauthenticate //The user must reauthenticate 14 | } 15 | -------------------------------------------------------------------------------- /AirMessage/Controllers/AccessibilityAccessViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccessibilityAccessViewController.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-12-04. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class AccessibilityAccessViewController: NSViewController { 12 | @IBOutlet weak var imageView: NSImageView! 13 | 14 | //macOS 13 doesn't have a lock in System Settings 15 | @IBOutlet weak var lockText: NSView! //OS X 10.10 to 12 16 | 17 | public var onDone: (() -> Void)? 18 | 19 | override func viewDidAppear() { 20 | super.viewDidAppear() 21 | 22 | //Set the window title 23 | view.window!.title = NSLocalizedString("label.accessibility_access", comment: "") 24 | 25 | //Use instructions for System Settings on macOS 13+ 26 | if #available(macOS 13.0, *) { 27 | imageView.image = NSImage(named: "AccessibilityAccess-13") 28 | lockText.isHidden = true 29 | } else { 30 | imageView.image = NSImage(named: "AccessibilityAccess") 31 | } 32 | } 33 | 34 | @IBAction func onOpenAccessibilityAccess(_ sender: Any) { 35 | NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!) 36 | } 37 | 38 | @IBAction func onClickDone(_ sender: Any) { 39 | view.window!.close() 40 | onDone?() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /AirMessage/Controllers/AccountConnectViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountConnectViewController.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-01-04. 6 | // 7 | 8 | import AppKit 9 | import AuthenticationServices 10 | import AppAuth 11 | 12 | private let oidServiceConfiguration = OIDServiceConfiguration( 13 | authorizationEndpoint: URL(string: "https://accounts.google.com/o/oauth2/auth")!, 14 | tokenEndpoint: URL(string: "https://oauth2.googleapis.com/token")! 15 | ) 16 | 17 | class AccountConnectViewController: NSViewController { 18 | @IBOutlet weak var cancelButton: NSButton! 19 | @IBOutlet weak var loadingProgressIndicator: NSProgressIndicator! 20 | @IBOutlet weak var loadingLabel: NSTextField! 21 | 22 | let redirectHandler = OIDRedirectHTTPHandler(successURL: nil) 23 | 24 | private var isConnecting = false 25 | private var currentDataProxy: DataProxyConnect! 26 | private var currentUserID: String! 27 | private var currentEmailAddress: String! 28 | 29 | public var onAccountConfirm: ((_ userID: String, _ emailAddress: String) -> Void)? 30 | 31 | override func viewDidLoad() { 32 | //Prevent resizing 33 | preferredContentSize = view.frame.size 34 | } 35 | 36 | override func viewDidAppear() { 37 | super.viewDidAppear() 38 | 39 | //Start the HTTP server 40 | var httpListenerError: NSError? 41 | let redirectURL = redirectHandler.startHTTPListener(&httpListenerError) 42 | if let httpListenerError = httpListenerError { 43 | try! { throw httpListenerError }() 44 | } 45 | 46 | //Start the authorization flow 47 | let request = OIDAuthorizationRequest( 48 | configuration: oidServiceConfiguration, 49 | clientId: (Bundle.main.infoDictionary!["GOOGLE_OAUTH_CLIENT_ID"] as! String), 50 | clientSecret: (Bundle.main.infoDictionary!["GOOGLE_OAUTH_CLIENT_SECRET"] as! String), 51 | scopes: [ 52 | "openid", 53 | "https://www.googleapis.com/auth/userinfo.email", 54 | "profile" 55 | ], 56 | redirectURL: redirectURL, 57 | responseType: "code", 58 | additionalParameters: ["prompt": "select_account"] 59 | ) 60 | 61 | redirectHandler.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: view.window!) { [weak self] result, error in 62 | DispatchQueue.main.async { [weak self] in 63 | guard let self = self else { return } 64 | 65 | //Surface errors to the user 66 | if let error = error as? NSError { 67 | //Don't show cancellation errors 68 | if error.domain == OIDGeneralErrorDomain { 69 | if error.code == OIDErrorCode.userCanceledAuthorizationFlow.rawValue { 70 | //If the user cancelled, dismiss the view controller 71 | self.dismiss(self) 72 | } else if error.code == OIDErrorCode.programCanceledAuthorizationFlow.rawValue { 73 | //If we cancelled (for example, the user closed the window), do nothing 74 | return 75 | } 76 | } else { 77 | //Present the error to the user 78 | self.showError(message: error.localizedDescription, showReconnect: false) 79 | } 80 | 81 | return 82 | } 83 | 84 | //Start a connection with the ID token 85 | let idToken = result!.lastTokenResponse!.idToken! 86 | self.startConnection(idToken: idToken, callbackURL: redirectURL.absoluteString) 87 | } 88 | } 89 | 90 | //Update the view 91 | setLoading(false) 92 | loadingProgressIndicator.startAnimation(self) 93 | 94 | //Register for authentication and connection updates 95 | NotificationCenter.default.addObserver(self, selector: #selector(onUpdateServerState), name: NotificationNames.updateServerState, object: nil) 96 | } 97 | 98 | override func viewDidDisappear() { 99 | //Stop the server 100 | redirectHandler.cancelHTTPListener() 101 | 102 | //Remove update listeners 103 | NotificationCenter.default.removeObserver(self) 104 | } 105 | 106 | /** 107 | Sets the view controller's views to reflect the loading state 108 | */ 109 | private func setLoading(_ loading: Bool) { 110 | cancelButton.isEnabled = !loading 111 | loadingLabel.isHidden = !loading 112 | } 113 | 114 | private func startConnection(idToken: String, callbackURL: String) { 115 | //Set the loading state 116 | setLoading(true) 117 | 118 | //Exchange the refresh token 119 | exchangeFirebaseIDPToken(idToken, providerID: "google.com", callbackURL: callbackURL) { [weak self] result, error in 120 | DispatchQueue.main.async { [weak self] in 121 | guard let self = self else { return } 122 | 123 | //Check for errors 124 | if let error = error { 125 | LogManager.log("Failed to exchange IDP token: \(error)", level: .info) 126 | self.showError(message: NSLocalizedString("message.register.error.sign_in", comment: ""), showReconnect: false) 127 | return 128 | } 129 | 130 | //If the error is nil, the result can't be nil 131 | let result = result! 132 | let userID = result.localId 133 | let idToken = result.idToken 134 | 135 | //Set the data proxy and connect 136 | self.isConnecting = true 137 | self.currentUserID = result.localId 138 | self.currentEmailAddress = result.email 139 | 140 | let proxy = DataProxyConnect(userID: userID, idToken: idToken) 141 | self.currentDataProxy = proxy 142 | ConnectionManager.shared.setProxy(proxy) 143 | ConnectionManager.shared.start() 144 | } 145 | } 146 | } 147 | 148 | /** 149 | Shows an alert dialog that informs the user of a successful connection 150 | */ 151 | private func showSuccess() { 152 | let alert = NSAlert() 153 | alert.alertStyle = .informational 154 | alert.messageText = NSLocalizedString("message.register.success.title", comment: "") 155 | alert.informativeText = NSLocalizedString("message.register.success.description", comment: "") 156 | alert.beginSheetModal(for: view.window!) { response in 157 | self.dismiss(self) 158 | self.onAccountConfirm?(self.currentUserID, self.currentEmailAddress) 159 | } 160 | } 161 | 162 | /** 163 | Shows an alert dialog that informs the user of an error 164 | - Parameters: 165 | - message: The message to display to the user 166 | - showReconnect: Whether to show a retry button that restarts the server 167 | */ 168 | private func showError(message: String, showReconnect: Bool) { 169 | let alert = NSAlert() 170 | alert.alertStyle = .warning 171 | alert.messageText = NSLocalizedString("message.register.error.title", comment: "") 172 | alert.informativeText = message 173 | if showReconnect { 174 | alert.addButton(withTitle: NSLocalizedString("action.retry", comment: "")) 175 | } 176 | alert.addButton(withTitle: NSLocalizedString("action.cancel", comment: "")) 177 | alert.beginSheetModal(for: view.window!) { response in 178 | if showReconnect && response == .alertFirstButtonReturn { 179 | //Reconnect and try again 180 | self.isConnecting = true 181 | ConnectionManager.shared.start() 182 | } else { 183 | //Dismiss the dialog 184 | self.dismiss(self) 185 | } 186 | } 187 | } 188 | 189 | @objc private func onUpdateServerState(notification: NSNotification) { 190 | guard isConnecting else { return } 191 | 192 | let serverState = ServerState(rawValue: notification.userInfo![NotificationNames.updateServerStateParam] as! Int)! 193 | if serverState == .running { 194 | //Set the data proxy as registered 195 | self.currentDataProxy.setRegistered() 196 | 197 | //Show a success dialog and close the view 198 | showSuccess() 199 | 200 | isConnecting = false 201 | } else if serverState.isError { 202 | //Show an error dialog 203 | showError(message: serverState.description, showReconnect: serverState.recoveryType == .retry) 204 | 205 | isConnecting = false 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /AirMessage/Controllers/AutomationAccessViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutomationAccessViewController.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-11. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class AutomationAccessViewController: NSViewController { 12 | @IBOutlet weak var imageView: NSImageView! 13 | 14 | public var onDone: (() -> Void)? 15 | 16 | override func viewDidAppear() { 17 | super.viewDidAppear() 18 | 19 | //Set the window title 20 | view.window!.title = NSLocalizedString("label.automation_access", comment: "") 21 | 22 | //Update the image 23 | if #available(macOS 13.0, *) { 24 | imageView.image = NSImage(named: "AutomationAccess-13") 25 | } else { 26 | imageView.image = NSImage(named: "AutomationAccess") 27 | } 28 | } 29 | 30 | @IBAction func onOpenAutomationAccess(_ sender: Any) { 31 | NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation")!) 32 | } 33 | 34 | @IBAction func onClickDone(_ sender: Any) { 35 | view.window!.close() 36 | onDone?() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AirMessage/Controllers/ClientListViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientList.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-31. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class ClientListViewController: NSViewController { 12 | //Keep in memory for older versions of OS X 13 | private static var clientListWindowController: NSWindowController? 14 | 15 | private static let cellID = NSUserInterfaceItemIdentifier(rawValue: "DeviceTableCell") 16 | 17 | @IBOutlet weak var tableView: NSTableView! 18 | 19 | private var clients: [ClientConnection.Registration]! 20 | 21 | static func open() { 22 | //If we're already showing the window, just focus it 23 | if let window = clientListWindowController?.window, window.isVisible { 24 | window.makeKeyAndOrderFront(self) 25 | NSApp.activate(ignoringOtherApps: true) 26 | return 27 | } 28 | 29 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 30 | let windowController = storyboard.instantiateController(withIdentifier: "ClientList") as! NSWindowController 31 | windowController.showWindow(nil) 32 | clientListWindowController = windowController 33 | NSApp.activate(ignoringOtherApps: true) 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | 39 | //Load data 40 | clients = ConnectionManager.shared.connections 41 | .map { connSet in 42 | connSet.sorted { $0.id > $1.id } 43 | .compactMap { $0.registration } 44 | } ?? [] 45 | 46 | //Set table delegate 47 | tableView.delegate = self 48 | tableView.dataSource = self 49 | } 50 | 51 | override func viewDidAppear() { 52 | super.viewDidAppear() 53 | 54 | //Set the window title 55 | view.window!.title = NSLocalizedString("label.client_log", comment: "") 56 | 57 | //Focus app 58 | NSApp.activate(ignoringOtherApps: true) 59 | } 60 | } 61 | 62 | extension ClientListViewController: NSTableViewDataSource { 63 | func numberOfRows(in tableView: NSTableView) -> Int { 64 | clients.count 65 | } 66 | } 67 | 68 | extension ClientListViewController: NSTableViewDelegate { 69 | public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { 70 | let cell = tableView.makeView(withIdentifier: ClientListViewController.cellID, owner: self) as! ClientTableCellView 71 | 72 | guard row < clients.count else { 73 | return nil 74 | } 75 | let client = clients[row] 76 | 77 | let image: NSImage 78 | switch client.platformID { 79 | case "android": 80 | image = NSImage(named: "Android")! 81 | case "windows": 82 | image = NSImage(named: "Windows")! 83 | case "chrome": 84 | image = NSImage(named: "BrowserChrome")! 85 | case "firefox": 86 | image = NSImage(named: "BrowserFirefox")! 87 | case "edge": 88 | image = NSImage(named: "BrowserEdge")! 89 | case "opera": 90 | image = NSImage(named: "BrowserOpera")! 91 | case "samsunginternet": 92 | image = NSImage(named: "BrowserSamsung")! 93 | case "safari": 94 | image = NSImage(named: "BrowserSafari")! 95 | default: 96 | image = NSImage(named: NSImage.networkName)! 97 | } 98 | 99 | cell.icon.image = image 100 | cell.title.stringValue = client.clientName 101 | cell.subtitle.stringValue = NSLocalizedString("label.currently_connected", comment: "") 102 | 103 | return cell 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /AirMessage/Controllers/FullDiskAccessViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FullDiskAccessViewController.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-11. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class FullDiskAccessViewController: NSViewController { 12 | @IBOutlet weak var imageView: NSImageView! 13 | 14 | //macOS 13 doesn't have a lock in System Settings 15 | @IBOutlet weak var lockText: NSView! //OS X 10.10 to 12 16 | 17 | /* 18 | Older versions of OS X don't support drag-and-drop for file URLs 19 | */ 20 | @IBOutlet weak var yosemiteText: NSView! //OS X 10.10 to 10.12 21 | @IBOutlet weak var highSierraText: NSView! //OS X 10.13+ 22 | @IBOutlet weak var highSierraDraggable: NSView! //OS X 10.13+ 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | //Use instructions for System Settings on macOS 13+ 28 | if #available(macOS 13.0, *) { 29 | imageView.image = NSImage(named: "FullDiskAccess-13") 30 | lockText.isHidden = true 31 | yosemiteText.isHidden = true 32 | } else if #available(macOS 10.13, *) { 33 | yosemiteText.isHidden = true 34 | } else { 35 | //Dragging isn't supported 36 | highSierraText.isHidden = true 37 | highSierraDraggable.isHidden = true 38 | } 39 | } 40 | 41 | override func viewDidAppear() { 42 | super.viewDidAppear() 43 | 44 | //Set the window title 45 | view.window!.title = NSLocalizedString("label.full_disk_access", comment: "") 46 | } 47 | 48 | @IBAction func onOpenFullDiskAccess(_ sender: Any) { 49 | NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles")!) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /AirMessage/Controllers/OnboardingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewControllerOnboarding.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-01-03. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class OnboardingViewController: NSViewController { 12 | //Keep in memory for older versions of OS X 13 | private static var onboardingWindowController: NSWindowController! 14 | 15 | static func open() { 16 | //If we're already showing the window, just focus it 17 | if let window = onboardingWindowController?.window, window.isVisible { 18 | window.makeKeyAndOrderFront(self) 19 | NSApp.activate(ignoringOtherApps: true) 20 | return 21 | } 22 | 23 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 24 | let windowController = storyboard.instantiateController(withIdentifier: "Onboarding") as! NSWindowController 25 | windowController.showWindow(nil) 26 | onboardingWindowController = windowController 27 | NSApp.activate(ignoringOtherApps: true) 28 | } 29 | 30 | override func viewWillAppear() { 31 | let window = view.window! 32 | window.isMovableByWindowBackground = true 33 | window.titlebarAppearsTransparent = true 34 | window.titleVisibility = .hidden 35 | } 36 | 37 | override func shouldPerformSegue(withIdentifier identifier: NSStoryboardSegue.Identifier, sender: Any?) -> Bool { 38 | if identifier == "PasswordEntry" { 39 | //Make sure Keychain is initialized 40 | do { 41 | try PreferencesManager.shared.initializeKeychain() 42 | } catch { 43 | KeychainManager.getErrorAlert(error).beginSheetModal(for: self.view.window!) 44 | return false 45 | } 46 | 47 | return true 48 | } else { 49 | return super.shouldPerformSegue(withIdentifier: identifier, sender: sender) 50 | } 51 | } 52 | 53 | override func prepare(for segue: NSStoryboardSegue, sender: Any?) { 54 | if segue.identifier == "PasswordEntry" { 55 | let passwordEntry = segue.destinationController as! PasswordEntryViewController 56 | 57 | //Password is required for manual setup 58 | passwordEntry.isRequired = true 59 | passwordEntry.onSubmit = { [weak self] password in 60 | guard let self = self else { return } 61 | //Save password and reset server port 62 | do { 63 | try PreferencesManager.shared.setPassword(password) 64 | } catch { 65 | KeychainManager.getErrorAlert(error).beginSheetModal(for: self.view.window!) 66 | return 67 | } 68 | PreferencesManager.shared.serverPort = defaultServerPort 69 | 70 | //Set the account type 71 | PreferencesManager.shared.accountType = .direct 72 | 73 | //Set the data proxy 74 | ConnectionManager.shared.setProxy(DataProxyTCP(port: defaultServerPort)) 75 | 76 | //Disable setup mode 77 | NotificationNames.postUpdateSetupMode(false) 78 | 79 | //Start server 80 | launchServer() 81 | 82 | //Close window 83 | self.view.window!.close() 84 | } 85 | } else if segue.identifier == "AccountConnect" { 86 | let accountConnect = segue.destinationController as! AccountConnectViewController 87 | accountConnect.onAccountConfirm = { [weak self] userID, emailAddress in 88 | //Dismiss the connect view 89 | accountConnect.dismiss(self) 90 | 91 | //Save the user ID and email address 92 | PreferencesManager.shared.connectUserID = userID 93 | PreferencesManager.shared.connectEmailAddress = emailAddress 94 | 95 | //Set the account type 96 | PreferencesManager.shared.accountType = .connect 97 | 98 | //Disable setup mode 99 | NotificationNames.postUpdateSetupMode(false) 100 | 101 | //Run permissions check 102 | if checkServerPermissions() { 103 | //Connect to the database 104 | do { 105 | try DatabaseManager.shared.start() 106 | } catch { 107 | LogManager.log("Failed to start database: \(error)", level: .notice) 108 | ConnectionManager.shared.stop() 109 | } 110 | 111 | //Start listening for FaceTime calls 112 | if FaceTimeHelper.isSupported && PreferencesManager.shared.faceTimeIntegration { 113 | FaceTimeHelper.startIncomingCallTimer() 114 | } 115 | } else { 116 | //Disconnect and let the user resolve the error 117 | ConnectionManager.shared.stop() 118 | } 119 | 120 | //Close window 121 | if let self = self { 122 | self.view.window!.close() 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /AirMessage/Controllers/PasswordEntryViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordEntry.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-01-04. 6 | // 7 | 8 | import AppKit 9 | 10 | class PasswordEntryViewController: NSViewController { 11 | @IBOutlet weak var secureField: NSSecureTextField! 12 | @IBOutlet weak var plainField: NSTextField! 13 | 14 | @IBOutlet weak var strengthLabel: NSTextField! 15 | @IBOutlet weak var passwordToggle: NSButton! 16 | 17 | @IBOutlet weak var confirmButton: NSButton! 18 | 19 | private var currentTextField: NSTextField! 20 | 21 | public var isRequired = true 22 | public var onSubmit: ((String) -> Void)? 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | 27 | secureField.delegate = self 28 | plainField.delegate = self 29 | 30 | currentTextField = secureField 31 | currentTextField.stringValue = PreferencesManager.shared.password 32 | 33 | confirmButton.isEnabled = !secureField.stringValue.isEmpty 34 | 35 | //Perform initial UI update 36 | updateUI() 37 | } 38 | 39 | @IBAction func onClickPasswordVisibility(_ sender: NSButton) { 40 | //Toggle password visibility 41 | if sender.state == .on { 42 | secureField.isHidden = true 43 | plainField.isHidden = false 44 | plainField.stringValue = secureField.stringValue 45 | plainField.becomeFirstResponder() 46 | 47 | currentTextField = plainField 48 | } else { 49 | secureField.isHidden = false 50 | plainField.isHidden = true 51 | secureField.stringValue = plainField.stringValue 52 | secureField.becomeFirstResponder() 53 | 54 | currentTextField = secureField 55 | } 56 | } 57 | 58 | func updateUI() { 59 | //Disable the button if there is no password and the password is required 60 | confirmButton.isEnabled = !isRequired || !getText().isEmpty 61 | 62 | //Update the password strength label 63 | strengthLabel.stringValue = String(format: NSLocalizedString("label.password_strength", comment: ""), getPasswordStrengthLabel(calculatePasswordStrength(getText()))) 64 | } 65 | 66 | func getText() -> String { currentTextField.stringValue } 67 | 68 | @IBAction func onClickConfirm(_ sender: NSButton) { 69 | onSubmit?(getText()) 70 | 71 | dismiss(self) 72 | } 73 | } 74 | 75 | extension PasswordEntryViewController: NSTextFieldDelegate { 76 | func controlTextDidChange(_ obj: Notification) { 77 | updateUI() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /AirMessage/Controllers/SoftwareUpdateProgressViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-10-08. 3 | // 4 | 5 | import Foundation 6 | import AppKit 7 | 8 | class SoftwareUpdateProgressViewController: NSViewController { 9 | @IBOutlet weak var label: NSTextField! 10 | @IBOutlet weak var progressIndicator: NSProgressIndicator! 11 | } 12 | -------------------------------------------------------------------------------- /AirMessage/Controllers/SoftwareUpdateViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-10-03. 3 | // 4 | 5 | import Foundation 6 | import AppKit 7 | import WebKit 8 | import Ink 9 | 10 | class SoftwareUpdateViewController: NSViewController { 11 | //Outlets 12 | @IBOutlet weak var descriptionLabel: NSTextField! 13 | @IBOutlet weak var webViewContainer: NSView! 14 | 15 | //Parameters 16 | public var updateData: UpdateStruct! 17 | 18 | //State 19 | private var sheetController: SoftwareUpdateProgressViewController? 20 | 21 | override func viewWillAppear() { 22 | super.viewWillAppear() 23 | 24 | //Create the WebView 25 | let webView = WKWebView() 26 | webView.frame = webViewContainer.bounds 27 | webView.navigationDelegate = self 28 | webView.wantsLayer = true 29 | webView.layer!.borderWidth = 1 30 | webView.layer!.borderColor = NSColor.lightGray.cgColor 31 | webViewContainer.addSubview(webView) 32 | webViewContainer.autoresizesSubviews = true 33 | 34 | //Load the update notes 35 | let parser = MarkdownParser() 36 | let notesHTML = """ 37 | 38 | 39 | \(parser.html(from: updateData.notes)) 40 | 41 | """ 42 | webView.loadHTMLString(notesHTML, baseURL: nil) 43 | 44 | //Set the update description 45 | descriptionLabel.stringValue = String(format: NSLocalizedString("message.update.available", comment: ""), updateData.versionName, Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String) 46 | } 47 | 48 | override func viewDidAppear() { 49 | super.viewDidAppear() 50 | 51 | //Set the window title 52 | view.window!.title = NSLocalizedString("label.software_update", comment: "") 53 | 54 | //Focus app 55 | NSApp.activate(ignoringOtherApps: true) 56 | } 57 | 58 | @IBAction func onRemindLater(_ sender: Any) { 59 | //Close window 60 | view.window!.close() 61 | } 62 | 63 | @IBAction func onInstallUpdate(_ sender: Any) { 64 | if updateData.downloadType == .external { 65 | //Open the URL 66 | NSWorkspace.shared.open(updateData.downloadURL) 67 | 68 | //Close the window 69 | view.window!.close() 70 | } else { 71 | //Start installing the update 72 | let updateStarted = UpdateHelper.install( 73 | update: updateData, 74 | onProgress: { [weak self] progress in 75 | if let sheetController = self?.sheetController { 76 | //Update the progress bar 77 | sheetController.progressIndicator.doubleValue = progress 78 | } 79 | }, 80 | onSuccess: { [weak self] in 81 | guard let self = self else { return } 82 | 83 | //Dismiss the sheet 84 | if let theSheetController = self.sheetController { 85 | self.dismiss(theSheetController) 86 | self.sheetController = nil 87 | } 88 | 89 | //Close the window 90 | self.view.window!.close() 91 | }, 92 | onError: { [weak self] code, description in 93 | guard let self = self else { return } 94 | 95 | //Dismiss the sheet 96 | if let theSheetController = self.sheetController { 97 | self.dismiss(theSheetController) 98 | self.sheetController = nil 99 | } 100 | 101 | //Show an alert 102 | let alertMessage: String 103 | switch code { 104 | case .download: 105 | alertMessage = NSLocalizedString("message.update.error.download", comment: "") 106 | case .badPackage: 107 | alertMessage = NSLocalizedString("message.update.error.invalid_package", comment: "") 108 | case .internalError: 109 | alertMessage = NSLocalizedString("message.update.error.internal", comment: "") 110 | case .readOnlyVolume: 111 | alertMessage = NSLocalizedString("message.update.error.readonly_volume", comment: "") 112 | } 113 | 114 | let alert = NSAlert() 115 | alert.alertStyle = .critical 116 | alert.messageText = alertMessage 117 | alert.beginSheetModal(for: self.view.window!) 118 | } 119 | ) 120 | 121 | if updateStarted { 122 | //Show a progress popup 123 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 124 | let windowController = storyboard.instantiateController(withIdentifier: "SoftwareUpdateProgress") as! SoftwareUpdateProgressViewController 125 | presentAsSheet(windowController) 126 | sheetController = windowController 127 | } 128 | } 129 | } 130 | } 131 | 132 | extension SoftwareUpdateViewController: WKNavigationDelegate { 133 | //Open clicked links in the browser 134 | func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 135 | if navigationAction.navigationType == .linkActivated, let url = navigationAction.request.url { 136 | NSWorkspace.shared.open(url) 137 | decisionHandler(.cancel) 138 | } else if navigationAction.navigationType == .reload { 139 | decisionHandler(.cancel) 140 | } else { 141 | decisionHandler(.allow) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /AirMessage/Data/DBFetchGrouping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DBFetchGrouping.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-15. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A result for database operations that return conversation items and loose modifiers 12 | */ 13 | struct DBFetchGrouping { 14 | let conversationItems: [ConversationItem] 15 | let looseModifiers: [ModifierInfo] 16 | 17 | var destructured: ([ConversationItem], [ModifierInfo]) { 18 | return (conversationItems, looseModifiers) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AirMessage/Data/MessageTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-12. 3 | // 4 | 5 | import Foundation 6 | 7 | //MARK: - iMessage 8 | 9 | protocol BaseConversationInfo: Packable { 10 | var guid: String { get } 11 | } 12 | 13 | struct ConversationInfo: BaseConversationInfo { 14 | let guid: String 15 | let service: String 16 | let name: String? 17 | let members: [String] 18 | 19 | func pack(to packer: inout AirPacker) { 20 | packer.pack(string: guid) 21 | packer.pack(bool: true) //Conversation available 22 | packer.pack(string: service) 23 | packer.pack(optionalString: name) 24 | packer.pack(stringArray: members) 25 | } 26 | } 27 | 28 | struct UnavailableConversationInfo: BaseConversationInfo { 29 | let guid: String 30 | 31 | func pack(to packer: inout AirPacker) { 32 | packer.pack(string: guid) 33 | packer.pack(bool: false) //Conversation unavailable 34 | } 35 | } 36 | 37 | struct LiteConversationInfo: Packable { 38 | let guid: String 39 | let service: String 40 | let name: String? 41 | let members: [String] 42 | 43 | let previewDate: Int64 44 | let previewSender: String? 45 | let previewText: String? 46 | let previewSendStyle: String? 47 | let previewAttachments: [String] 48 | 49 | func pack(to packer: inout AirPacker) { 50 | packer.pack(string: guid) 51 | packer.pack(string: service) 52 | packer.pack(optionalString: name) 53 | packer.pack(stringArray: members) 54 | 55 | packer.pack(long: previewDate) 56 | packer.pack(optionalString: previewSender) 57 | packer.pack(optionalString: previewText) 58 | packer.pack(optionalString: previewSendStyle) 59 | packer.pack(stringArray: previewAttachments) 60 | } 61 | } 62 | 63 | protocol ConversationItem: Packable { 64 | static var itemType: Int32 {get} 65 | 66 | var serverID: Int64 { get } 67 | var guid: String { get } 68 | var chatGUID: String { get } 69 | var date: Int64 { get } 70 | } 71 | 72 | extension ConversationItem { 73 | func packBase(to packer: inout AirPacker) { 74 | packer.pack(int: Self.itemType) 75 | 76 | packer.pack(long: serverID) 77 | packer.pack(string: guid) 78 | packer.pack(string: chatGUID) 79 | packer.pack(long: date) 80 | } 81 | } 82 | 83 | struct MessageInfo: ConversationItem { 84 | static let itemType: Int32 = 0 85 | 86 | let serverID: Int64 87 | let guid: String 88 | let chatGUID: String 89 | let date: Int64 90 | 91 | enum State: Int32 { 92 | case idle = 0 93 | case sent = 1 94 | case delivered = 2 95 | case read = 3 96 | } 97 | enum Error: Int32 { 98 | case ok = 0 99 | case unknown = 1 //Unknown error code 100 | case network = 2 //Network error 101 | case unregistered = 3 //Not registered with iMessage 102 | } 103 | 104 | let text: String? 105 | let subject: String? 106 | let sender: String? 107 | var attachments: [AttachmentInfo] 108 | var stickers: [StickerModifierInfo] 109 | var tapbacks: [TapbackModifierInfo] 110 | let sendEffect: String? 111 | let state: State 112 | let error: Error 113 | let dateRead: Int64 114 | 115 | func pack(to packer: inout AirPacker) { 116 | packBase(to: &packer) 117 | 118 | packer.pack(optionalString: text) 119 | packer.pack(optionalString: subject) 120 | packer.pack(optionalString: sender) 121 | packer.pack(packableArray: attachments) 122 | packer.pack(packableArray: stickers) 123 | packer.pack(packableArray: tapbacks) 124 | packer.pack(optionalString: sendEffect) 125 | packer.pack(int: state.rawValue) 126 | packer.pack(int: error.rawValue) 127 | packer.pack(long: dateRead) 128 | } 129 | } 130 | 131 | struct GroupActionInfo: ConversationItem { 132 | static let itemType: Int32 = 1 133 | 134 | let serverID: Int64 135 | let guid: String 136 | let chatGUID: String 137 | let date: Int64 138 | 139 | enum Subtype: Int32 { 140 | case unknown = 0 141 | case join = 1 142 | case leave = 2 143 | } 144 | 145 | let agent: String? 146 | let other: String? 147 | let subtype: Subtype 148 | 149 | func pack(to packer: inout AirPacker) { 150 | packBase(to: &packer) 151 | 152 | packer.pack(optionalString: agent) 153 | packer.pack(optionalString: other) 154 | packer.pack(int: subtype.rawValue) 155 | } 156 | } 157 | 158 | struct ChatRenameActionInfo: ConversationItem { 159 | static let itemType: Int32 = 2 160 | 161 | let serverID: Int64 162 | let guid: String 163 | let chatGUID: String 164 | let date: Int64 165 | 166 | let agent: String? 167 | let updatedName: String? 168 | 169 | func pack(to packer: inout AirPacker) { 170 | packBase(to: &packer) 171 | 172 | packer.pack(optionalString: agent) 173 | packer.pack(optionalString: updatedName) 174 | } 175 | } 176 | 177 | struct AttachmentInfo: Packable { 178 | let guid: String 179 | let name: String 180 | let type: String? 181 | let size: Int64 182 | let checksum: Data? 183 | let sort: Int64 184 | let localURL: URL? 185 | 186 | func pack(to packer: inout AirPacker) { 187 | packer.pack(string: guid) 188 | packer.pack(string: name) 189 | packer.pack(optionalString: type) 190 | packer.pack(long: size) 191 | packer.pack(optionalPayload: checksum) 192 | packer.pack(long: sort) 193 | } 194 | } 195 | 196 | protocol ModifierInfo: Packable { 197 | static var itemType: Int32 { get } 198 | var messageGUID: String { get } 199 | } 200 | 201 | extension ModifierInfo { 202 | func packBase(to packer: inout AirPacker) { 203 | packer.pack(int: Self.itemType) 204 | packer.pack(string: messageGUID) 205 | } 206 | } 207 | 208 | struct ActivityStatusModifierInfo: ModifierInfo { 209 | static let itemType: Int32 = 0 210 | let messageGUID: String 211 | 212 | let state: MessageInfo.State 213 | let dateRead: Int64 214 | 215 | func pack(to packer: inout AirPacker) { 216 | packBase(to: &packer) 217 | 218 | packer.pack(int: state.rawValue) 219 | packer.pack(long: dateRead) 220 | } 221 | } 222 | 223 | struct StickerModifierInfo: ModifierInfo { 224 | static let itemType: Int32 = 1 225 | let messageGUID: String 226 | 227 | let messageIndex: Int32 228 | let fileGUID: String 229 | let sender: String? 230 | let date: Int64 231 | let data: Data 232 | let type: String 233 | 234 | func pack(to packer: inout AirPacker) { 235 | packBase(to: &packer) 236 | 237 | packer.pack(int: messageIndex) 238 | packer.pack(string: fileGUID) 239 | packer.pack(optionalString: sender) 240 | packer.pack(long: date) 241 | packer.pack(payload: data) 242 | packer.pack(string: type) 243 | } 244 | } 245 | 246 | struct TapbackModifierInfo: ModifierInfo { 247 | static let itemType: Int32 = 2 248 | let messageGUID: String 249 | 250 | let messageIndex: Int32 251 | let sender: String? 252 | let isAddition: Bool 253 | let tapbackType: Int32 254 | 255 | func pack(to packer: inout AirPacker) { 256 | packBase(to: &packer) 257 | 258 | packer.pack(int: messageIndex) 259 | packer.pack(optionalString: sender) 260 | packer.pack(bool: isAddition) 261 | packer.pack(int: tapbackType) 262 | } 263 | } 264 | 265 | //MARK: - FaceTime 266 | 267 | struct CallHistoryEntry: Packable { 268 | let outgoing: Bool 269 | let answered: Bool 270 | let date: Int64 271 | let participants: [String] 272 | 273 | func pack(to packer: inout AirPacker) { 274 | packer.pack(bool: outgoing) 275 | packer.pack(bool: answered) 276 | packer.pack(long: date) 277 | packer.pack(stringArray: participants) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /AirMessage/Data/UpdateErrorCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-10-23. 3 | // 4 | 5 | import Foundation 6 | 7 | enum UpdateErrorCode: Int { 8 | case download = 0 9 | case badPackage = 1 10 | case internalError = 2 11 | case readOnlyVolume = 3 12 | } 13 | -------------------------------------------------------------------------------- /AirMessage/Data/UpdateStruct.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-10-03. 3 | // 4 | 5 | import Foundation 6 | 7 | struct UpdateStruct { 8 | enum DownloadType { 9 | //This update can be downloaded and installed remotely 10 | case remote 11 | 12 | //This update can be downloaded and installed automatically, 13 | //but only if the user is present at the computer 14 | case local 15 | 16 | //This update will be downloaded through the browser, and must be 17 | //installed manually by the user 18 | case external 19 | } 20 | 21 | let id: Int32 22 | let protocolRequirement: [Int32] 23 | let versionCode: Int32 24 | let versionName: String 25 | let notes: String 26 | let downloadURL: URL 27 | let downloadType: DownloadType 28 | } 29 | -------------------------------------------------------------------------------- /AirMessage/Database/DatabaseTimeConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseManager.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-11. 6 | // 7 | 8 | import Foundation 9 | 10 | private enum TimeSystem { 11 | case cocoaCoreData //Cocoa core data / Seconds since January 1st, 2001, 00:00, UTC-0 12 | case macAbsoluteTime //Mac absolute time / Nanoseconds since January 1st, 2001, 00:00, UTC-0 13 | } 14 | 15 | /* 16 | Mac Absolute Time is used in the Messages database on macOS 10.13 and above 17 | Earlier system versions use Cocoa Core Data 18 | */ 19 | private var system: TimeSystem { 20 | if #available(macOS 10.13, *) { 21 | return .macAbsoluteTime 22 | } else { 23 | return .cocoaCoreData 24 | } 25 | } 26 | 27 | //The difference (in milliseconds) between the Unix epoch (1970) and Apple epoch (2001) 28 | private let appleUnixEpochDifference: Int64 = 978_307_200 * 1000 29 | 30 | /** 31 | Gets the current time in database time 32 | */ 33 | func getDBTime() -> Int64 { 34 | if system == .macAbsoluteTime { 35 | //CoreFoundation time is in seconds, multiply to get nanoseconds 36 | return Int64(CFAbsoluteTimeGetCurrent() * 1e9) 37 | } else { 38 | return Int64(CFAbsoluteTimeGetCurrent()) 39 | } 40 | } 41 | 42 | /** 43 | Converts from UNIX time to database time 44 | */ 45 | func convertDBTime(fromUNIX time: Int64) -> Int64 { 46 | if system == .macAbsoluteTime { 47 | return (time - appleUnixEpochDifference) * 1_000_000 //Unix epoch -> Apple epoch, milliseconds -> nanoseconds 48 | } else { 49 | return (time - appleUnixEpochDifference) / 1000 //Unix epoch -> Apple epoch / milliseconds -> seconds 50 | } 51 | } 52 | 53 | /** 54 | Converts from database time to UNIX time 55 | */ 56 | func convertDBTime(fromDB time: Int64) -> Int64 { 57 | if system == .macAbsoluteTime { 58 | return time / 1_000_000 + appleUnixEpochDifference; //Nanoseconds -> milliseconds / Apple epoch -> Unix epoch 59 | } else { 60 | return time * 1000 + appleUnixEpochDifference; //Seconds -> milliseconds / Apple epoch -> Unix epoch 61 | } 62 | } -------------------------------------------------------------------------------- /AirMessage/Database/SQL/QueryAllChatDetails.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | chat.guid AS "chat.guid", 3 | chat.display_name AS "chat.display_name", 4 | chat.service_name AS "chat.service_name", 5 | GROUP_CONCAT(handle.id) AS member_list 6 | FROM chat 7 | JOIN chat_handle_join ON chat.ROWID = chat_handle_join.chat_id 8 | JOIN handle ON chat_handle_join.handle_id = handle.ROWID 9 | GROUP BY chat.ROWID 10 | -------------------------------------------------------------------------------- /AirMessage/Database/SQL/QueryAllChatDetailsSince.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | chat.guid AS "chat.guid", 3 | chat.display_name AS "chat.display_name", 4 | chat.service_name AS "chat.service_name", 5 | GROUP_CONCAT(handle.id) AS member_list 6 | FROM ( 7 | SELECT 8 | chat.*, 9 | MAX(message.date) AS last_date 10 | FROM message 11 | JOIN chat_message_join ON message.ROWID = chat_message_join.message_id 12 | JOIN chat ON chat_message_join.chat_id = chat.ROWID 13 | GROUP BY chat.ROWID 14 | ) AS chat 15 | JOIN chat_handle_join ON chat.ROWID = chat_handle_join.chat_id 16 | JOIN handle ON chat_handle_join.handle_id = handle.ROWID 17 | WHERE last_date > ? 18 | GROUP BY chat.ROWID 19 | -------------------------------------------------------------------------------- /AirMessage/Database/SQL/QueryAllChatSummary.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | chat.guid AS "chat.guid", 3 | chat.display_name AS "chat.display_name", 4 | chat.service_name AS "chat.service_name", 5 | message.attributedBody AS "message.attributedBody", 6 | message.date AS "message.date", 7 | message.is_from_me AS "message.is_from_me", 8 | handle.id AS "handle.id", 9 | sub2.member_list AS member_list, 10 | GROUP_CONCAT(attachment.mime_type) AS attachment_list 11 | %1$@ /* Extra fields, leading comma will be inserted */ 12 | FROM ( 13 | /* Join chats to members, concat to member_list */ 14 | SELECT 15 | sub1.*, 16 | GROUP_CONCAT(handle.id) AS member_list 17 | FROM ( 18 | /* Select the most recent message per chat */ 19 | SELECT 20 | chat.ROWID AS chat_id, 21 | message.ROWID AS message_id, 22 | MAX(message.date) 23 | FROM chat 24 | JOIN chat_message_join ON chat.ROWID = chat_message_join.chat_id 25 | JOIN message ON chat_message_join.message_id = message.ROWID 26 | WHERE message.item_type = 0 27 | GROUP BY chat.ROWID 28 | ) AS sub1 29 | JOIN chat_handle_join ON chat_handle_join.chat_id = sub1.chat_id 30 | JOIN handle ON chat_handle_join.handle_id = handle.ROWID 31 | GROUP BY sub1.chat_id 32 | ) AS sub2 33 | JOIN chat ON chat.ROWID = sub2.chat_id 34 | JOIN message ON message.ROWID = sub2.message_id 35 | LEFT JOIN message_attachment_join ON message_attachment_join.message_id = sub2.message_id 36 | LEFT JOIN attachment ON message_attachment_join.attachment_id = attachment.ROWID 37 | LEFT JOIN handle ON message.handle_id = handle.ROWID 38 | GROUP BY chat.ROWID 39 | ORDER BY message.date DESC 40 | -------------------------------------------------------------------------------- /AirMessage/Database/SQL/QueryMessageChatHandle.sql: -------------------------------------------------------------------------------- 1 | SELECT %1$@ /* Fields */ 2 | FROM message 3 | JOIN chat_message_join ON message.ROWID = chat_message_join.message_id 4 | JOIN chat ON chat_message_join.chat_id = chat.ROWID 5 | LEFT JOIN handle AS sender_handle ON message.handle_id = sender_handle.ROWID 6 | LEFT JOIN handle AS other_handle ON message.other_handle = other_handle.ROWID 7 | %2$@ /* Extra query statements */ 8 | -------------------------------------------------------------------------------- /AirMessage/Database/SQL/QueryOutgoingMessages.sql: -------------------------------------------------------------------------------- 1 | SELECT max(message.ROWID) as "message.ROWID", 2 | message.guid AS "message.guid", 3 | message.is_sent AS "message.is_sent", 4 | message.is_delivered AS "message.is_delivered", 5 | message.is_read AS "message.is_read", 6 | message.date_read AS "message.date_read", 7 | chat.ROWID AS "chat.ROWID" 8 | FROM chat 9 | LEFT OUTER JOIN chat_message_join ON chat.ROWID = chat_message_join.chat_id 10 | LEFT OUTER JOIN message ON chat_message_join.message_id = message.ROWID 11 | WHERE message.guid IS NOT NULL 12 | AND message.is_from_me IS 1 13 | %1$@ /* Extra query statements */ 14 | GROUP BY chat.ROWID 15 | -------------------------------------------------------------------------------- /AirMessage/Database/SQL/QuerySpecificChatDetails.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | chat.guid AS "chat.guid", 3 | chat.display_name AS "chat.display_name", 4 | chat.service_name AS "chat.service_name", 5 | GROUP_CONCAT(handle.id) AS member_list 6 | FROM chat 7 | JOIN chat_handle_join ON chat.ROWID = chat_handle_join.chat_id 8 | JOIN handle ON chat_handle_join.handle_id = handle.ROWID 9 | WHERE chat.guid IN (?) 10 | GROUP BY chat.ROWID 11 | -------------------------------------------------------------------------------- /AirMessage/Helper/ArchiveHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArchiveHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2022-08-13. 6 | // 7 | 8 | import Foundation 9 | 10 | func decompressArchive(fromURL: URL, to toURL: URL) throws { 11 | //Run ditto to extract the files 12 | let process = Process() 13 | if #available(macOS 10.13, *) { 14 | process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto") 15 | } else { 16 | process.launchPath = "/usr/bin/ditto" 17 | } 18 | process.arguments = ["-x", "-k", fromURL.path, toURL.path] 19 | try runProcessCatchError(process) 20 | } 21 | -------------------------------------------------------------------------------- /AirMessage/Helper/AssertionHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AssertionHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2022-05-31. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Adds a debug assertion that the code is being run on the passed dispatch queue 11 | func assertDispatchQueue(_ queue: DispatchQueue) { 12 | #if DEBUG 13 | if #available(macOS 10.12, *) { 14 | dispatchPrecondition(condition: .onQueue(queue)) 15 | } 16 | #endif 17 | } 18 | -------------------------------------------------------------------------------- /AirMessage/Helper/AtomicValue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AtomicValue.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-16. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A simple wrapper for a thread-safe value protected by a read-write lock 12 | */ 13 | class AtomicValue { 14 | private let lock = ReadWriteLock() 15 | private var _value: Value 16 | public var value: Value { 17 | get { 18 | lock.withReadLock { 19 | return _value 20 | } 21 | } 22 | set { 23 | lock.withWriteLock { 24 | _value = newValue 25 | } 26 | } 27 | } 28 | 29 | init(initialValue: Value) { 30 | _value = initialValue 31 | } 32 | 33 | /** 34 | Helper function that returns an inout for more advanced operations 35 | */ 36 | @discardableResult 37 | public func with(_ body: (inout Value) throws -> Result) rethrows -> Result { 38 | try lock.withWriteLock { 39 | try body(&_value) 40 | } 41 | } 42 | } 43 | 44 | typealias AtomicBool = AtomicValue 45 | -------------------------------------------------------------------------------- /AirMessage/Helper/CompressionHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-12. 3 | // 4 | 5 | import Foundation 6 | import Zlib 7 | 8 | private let minChunkSize = 1024 9 | 10 | /** 11 | Compresses the data with zlib 12 | - Parameter dataIn: The data to compress 13 | - Returns: The compressed data, or nil if an error occurred 14 | */ 15 | func compressData(_ dataIn: inout Data) -> Data? { 16 | //Initialize zlib stream 17 | var stream = z_stream() 18 | let initError = zlibInitializeDeflate(&stream) 19 | guard initError == Z_OK else { 20 | LogManager.log("Failed to initialize zlib stream: error code \(initError)", level: .error) 21 | return nil 22 | } 23 | defer { 24 | deflateEnd(&stream) 25 | } 26 | 27 | //Deflate the data 28 | var dataOut = Data(count: dataIn.count) 29 | let deflateError = dataIn.withUnsafeMutableBytes { (ptrIn: UnsafeMutableRawBufferPointer) -> Int32 in 30 | dataOut.withUnsafeMutableBytes { (ptrOut: UnsafeMutableRawBufferPointer) in 31 | stream.next_in = ptrIn.baseAddress!.assumingMemoryBound(to: Bytef.self) 32 | stream.avail_in = uInt(ptrIn.count) 33 | 34 | stream.next_out = ptrOut.baseAddress!.assumingMemoryBound(to: Bytef.self) 35 | stream.avail_out = uInt(ptrOut.count) 36 | 37 | return deflate(&stream, Z_FINISH) 38 | } 39 | } 40 | 41 | //Check for errors 42 | guard deflateError == Z_STREAM_END else { 43 | LogManager.log("Failed to deflate zlib: error code \(deflateError)", level: .error) 44 | return nil 45 | } 46 | 47 | //Return the data 48 | return dataOut.dropLast(Int(stream.avail_out)) 49 | } 50 | 51 | /** 52 | Pipes data through zlib deflate 53 | */ 54 | class CompressionPipeDeflate { 55 | private var stream: z_stream 56 | private let chunkSize: Int? 57 | 58 | /** 59 | Creates a new zlib deflate pipe. 60 | 61 | Setting the chunk size to the chunk size of each block of input is recommended, 62 | though it can be set to nil to automatically determine this value 63 | */ 64 | init(chunkSize: Int? = nil) throws { 65 | stream = z_stream() 66 | let initError = withUnsafeMutablePointer(to: &stream) { streamPtr in 67 | zlibInitializeDeflate(streamPtr) 68 | } 69 | guard initError == Z_OK else { 70 | throw CompressionError.zlibError(initError) 71 | } 72 | 73 | self.chunkSize = chunkSize 74 | } 75 | 76 | deinit { 77 | deflateEnd(&stream) 78 | } 79 | 80 | /** 81 | Pipes data through zlib to produce compressed output 82 | - Parameters: 83 | - dataIn: The data to compress 84 | - isLast: Whether this is the last data block 85 | */ 86 | func pipe(data dataIn: inout Data, isLast: Bool) throws -> Data { 87 | return try dataIn.withUnsafeMutableBytes { (ptrIn: UnsafeMutableRawBufferPointer) in 88 | //Set the input data 89 | stream.next_in = ptrIn.baseAddress!.assumingMemoryBound(to: Bytef.self) 90 | stream.avail_in = uInt(ptrIn.count) 91 | 92 | //Create the result data collector 93 | var dataReturn = Data() 94 | 95 | repeat { 96 | //Initialize the output data 97 | var dataOut: Data 98 | if let chunkSize = chunkSize { 99 | dataOut = Data(count: chunkSize) 100 | } else { 101 | dataOut = Data(count: max(ptrIn.count, minChunkSize)) 102 | } 103 | 104 | //Run deflate 105 | let deflateResult = dataOut.withUnsafeMutableBytes { (ptrOut: UnsafeMutableRawBufferPointer) -> Int32 in 106 | stream.next_out = ptrOut.baseAddress!.assumingMemoryBound(to: Bytef.self) 107 | stream.avail_out = uInt(ptrOut.count) 108 | 109 | return deflate(&stream, isLast ? Z_FINISH : Z_NO_FLUSH) 110 | } 111 | 112 | //Check the return code 113 | guard deflateResult != Z_STREAM_ERROR else { 114 | throw CompressionError.zlibError(deflateResult) 115 | } 116 | 117 | //Append the compressed data 118 | dataReturn.append(dataOut.dropLast(Int(stream.avail_out))) 119 | } while stream.avail_out == 0 120 | 121 | return dataReturn 122 | } 123 | } 124 | } 125 | 126 | /** 127 | Pipes data through zlib inflate 128 | */ 129 | class CompressionPipeInflate { 130 | private var stream: z_stream 131 | private(set) var isFinished = false 132 | private let chunkSize: Int? 133 | 134 | /** 135 | Creates a new zlib inflate pipe. 136 | 137 | Setting the chunk size to the chunk size of each block of input is recommended, 138 | though it can be set to nil to automatically determine this value 139 | */ 140 | init(chunkSize: Int? = nil) throws { 141 | stream = z_stream() 142 | let initError = zlibInitializeInflate(&stream) 143 | guard initError == Z_OK else { 144 | throw CompressionError.zlibError(initError) 145 | } 146 | self.chunkSize = chunkSize 147 | } 148 | 149 | deinit { 150 | inflateEnd(&stream) 151 | } 152 | 153 | /** 154 | Pipes data through zlib to produce decompressed output 155 | - Parameters: 156 | - dataIn: The data to compress 157 | */ 158 | func pipe(data dataIn: inout Data) throws -> Data { 159 | //If the stream is already finished, don't allow the input of any more data 160 | guard !isFinished else { 161 | throw CompressionError.streamFinished 162 | } 163 | 164 | return try dataIn.withUnsafeMutableBytes { (ptrIn: UnsafeMutableRawBufferPointer) in 165 | //Set the input data 166 | stream.next_in = ptrIn.baseAddress!.assumingMemoryBound(to: Bytef.self) 167 | stream.avail_in = uInt(ptrIn.count) 168 | 169 | //Create the result data collector 170 | var dataReturn = Data() 171 | 172 | repeat { 173 | //Initialize the output data 174 | var dataOut: Data 175 | if let chunkSize = chunkSize { 176 | dataOut = Data(count: chunkSize) 177 | } else { 178 | dataOut = Data(count: max(ptrIn.count, minChunkSize)) 179 | } 180 | 181 | //Run inflate 182 | let inflateResult = dataOut.withUnsafeMutableBytes { (ptrOut: UnsafeMutableRawBufferPointer) -> Int32 in 183 | stream.next_out = ptrOut.baseAddress!.assumingMemoryBound(to: Bytef.self) 184 | stream.avail_out = uInt(ptrOut.count) 185 | 186 | return inflate(&stream, Z_NO_FLUSH) 187 | } 188 | 189 | //Check the return code 190 | guard ![Z_STREAM_ERROR, Z_NEED_DICT, Z_DATA_ERROR, Z_MEM_ERROR].contains(inflateResult) else { 191 | throw CompressionError.zlibError(inflateResult) 192 | } 193 | 194 | //Set the finished flag if we reached the end of the stream 195 | if inflateResult == Z_STREAM_END { 196 | isFinished = true 197 | } 198 | 199 | //Append the decompressed data 200 | dataReturn.append(dataOut.dropLast(Int(stream.avail_out))) 201 | } while stream.avail_out == 0 202 | 203 | return dataReturn 204 | } 205 | } 206 | } 207 | 208 | enum CompressionError: Error { 209 | case zlibError(Int32) 210 | case streamFinished 211 | } 212 | -------------------------------------------------------------------------------- /AirMessage/Helper/ContentTypeHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentTypeHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Runs a simple comparison of 2 MIME types, returning if they overlap 12 | */ 13 | func compareMIMETypes(_ value1: String, _ value2: String) -> Bool { 14 | //Handle case where either type is a complete wildcard 15 | if value1 == "*/*" || value2 == "*/*" { 16 | return true 17 | } 18 | 19 | //Split MIME types into type and subtype 20 | let split1 = value1.split(separator: "/", maxSplits: 2) 21 | let split2 = value2.split(separator: "/", maxSplits: 2) 22 | 23 | //Make sure that we have 2 splits 24 | guard split1.count == 2 && split2.count == 2 else { 25 | return false 26 | } 27 | 28 | //If the subtype of either value is a wildcard, compare their main types 29 | if split1[1] == "*" || split2[1] == "*" { 30 | return split1[0] == split2[0] 31 | } 32 | 33 | //Just do a direct comparison 34 | return value1 == value2 35 | } 36 | -------------------------------------------------------------------------------- /AirMessage/Helper/CryptoHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-11-12. 3 | // 4 | 5 | import Foundation 6 | import CommonCrypto 7 | 8 | //https://stackoverflow.com/a/42935601 9 | func md5HashFile(url: URL) -> Data? { 10 | let bufferSize = 1024 * 1024 11 | 12 | do { 13 | //Open file for reading 14 | let file = try FileHandle(forReadingFrom: url) 15 | 16 | //Create and initialize the MD5 context 17 | var context = CC_MD5_CTX() 18 | CC_MD5_Init(&context) 19 | 20 | var doBreak: Bool 21 | repeat { 22 | doBreak = try autoreleasepool { 23 | //Read data 24 | let data = try file.readCompat(upToCount: bufferSize) 25 | 26 | //Check if there's any data to process 27 | if data.count > 0 { 28 | //Update the MD5 context 29 | data.withUnsafeBytes { 30 | _ = CC_MD5_Update(&context, $0.baseAddress, numericCast(data.count)) 31 | } 32 | 33 | //Continue 34 | return false 35 | } else { 36 | //End of file 37 | return true 38 | } 39 | } 40 | } while !doBreak 41 | 42 | //Compute the MD5 digest 43 | var digest = Data(count: Int(CC_MD5_DIGEST_LENGTH)) 44 | digest.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in 45 | _ = CC_MD5_Final(ptr.baseAddress!.assumingMemoryBound(to: UInt8.self), &context) 46 | } 47 | 48 | return digest 49 | } catch { 50 | LogManager.log("Failed to calculate MD5 hash: \(error)", level: .error) 51 | return nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /AirMessage/Helper/DispatchHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DispatchHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-18. 6 | // 7 | 8 | import Foundation 9 | 10 | ///Runs work on the main thread synchronously, skipping the dispatch queue if we're already on the main thread 11 | func runOnMain(execute work: () throws -> T) rethrows -> T { 12 | if Thread.isMainThread { 13 | return try work() 14 | } else { 15 | return try DispatchQueue.main.sync(execute: work) 16 | } 17 | } 18 | 19 | /// Runs work on the main thread asynchronously, skipping the dispatch queue if we're already on the main thread 20 | func runOnMainAsync(execute work: @escaping () -> Void) { 21 | if Thread.isMainThread { 22 | work() 23 | } else { 24 | DispatchQueue.main.async(execute: work) 25 | } 26 | } 27 | 28 | ///Runs work on the specified queue synchronously, skipping the dispatch queue if the key matches 29 | func runOnQueue(queue: DispatchQueue, key: DispatchSpecificKey, execute work: () throws -> T) rethrows -> T { 30 | if DispatchQueue.getSpecific(key: key) != nil { 31 | return try work() 32 | } else { 33 | return try queue.sync(execute: work) 34 | } 35 | } 36 | 37 | ///Runs work on the specified queue asynchronously, skipping the dispatch queue if the key matches 38 | func runOnQueueAsync(queue: DispatchQueue, key: DispatchSpecificKey, execute work: @escaping () -> Void) { 39 | if DispatchQueue.getSpecific(key: key) != nil { 40 | return work() 41 | } else { 42 | return queue.async(execute: work) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /AirMessage/Helper/FileNormalizationHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileNormalizationHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-15. 6 | // 7 | 8 | import Foundation 9 | import Sentry 10 | 11 | struct NormalizedFile { 12 | let url: URL 13 | let type: String 14 | let name: String 15 | } 16 | 17 | /** 18 | Converts a file from Apple-specific formats to formats that are readable by non-Apple devices. 19 | The resulting file is stored in a temporary location, and should be cleaned up after it is finished being used. 20 | This function does not modify or remove the input file. 21 | - Parameters: 22 | - inputFile: The file to process 23 | - extension: The extension type of the file 24 | - Returns: A tuple with the updated file path and string, or NIL if the file was not converted 25 | */ 26 | func normalizeFile(url inputFile: URL, ext: String) -> NormalizedFile? { 27 | /* 28 | These conversions are only available on macOS 10.13+, 29 | but older versions have these files converted before they 30 | even reach the device anyways 31 | */ 32 | guard #available(macOS 10.13, *) else { return nil } 33 | 34 | if ext.caseInsensitiveCompare("heic") == .orderedSame { 35 | LogManager.log("Converting file \(inputFile.lastPathComponent) from HEIC", level: .info) 36 | 37 | //Get a temporary file 38 | let tempFile = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString + ".jpeg") 39 | 40 | //Convert the file with sips 41 | let process = Process() 42 | process.executableURL = URL(fileURLWithPath: "/usr/bin/sips") 43 | process.arguments = ["--setProperty", "format", "jpeg", inputFile.path, "--out", tempFile.path] 44 | do { 45 | try runProcessCatchError(process) 46 | } catch { 47 | LogManager.log("Failed to convert file \(inputFile.path) from HEIC to JPEG: \(error)", level: .info) 48 | try? FileManager.default.removeItem(at: tempFile) //Clean up immediately 49 | return nil 50 | } 51 | 52 | //Build the new file name 53 | let newFileName = inputFile.deletingPathExtension().lastPathComponent + ".jpeg" 54 | 55 | return NormalizedFile(url: tempFile, type: "image/jpeg", name: newFileName) 56 | } else if ext.caseInsensitiveCompare("caf") == .orderedSame { 57 | LogManager.log("Converting file \(inputFile.lastPathComponent) from CAF", level: .info) 58 | 59 | //Get a temporary file 60 | let tempFile = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(UUID().uuidString + ".mp4") 61 | 62 | //Convert the file with sips 63 | let process = Process() 64 | process.executableURL = URL(fileURLWithPath: "/usr/bin/afconvert") 65 | process.arguments = ["-f", "mp4f", "-d", "aac", inputFile.path, "-o", tempFile.path] 66 | do { 67 | try runProcessCatchError(process) 68 | } catch { 69 | LogManager.log("Failed to convert file \(inputFile.path) from CAF to MP4: \(error)", level: .info) 70 | try? FileManager.default.removeItem(at: tempFile) //Clean up immediately 71 | return nil 72 | } 73 | 74 | //Build the new file name 75 | let newFileName = inputFile.deletingPathExtension().lastPathComponent + ".mp4" 76 | 77 | return NormalizedFile(url: tempFile, type: "audio/mp4", name: newFileName) 78 | } else { 79 | return nil 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /AirMessage/Helper/KeychainManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainManager.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-17. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | import AppKit 11 | 12 | private let keychainService = "AirMessage" 13 | 14 | class KeychainManager { 15 | private init() {} 16 | 17 | private static let queryBase: [String: Any] = [ 18 | String(kSecClass): kSecClassGenericPassword, 19 | String(kSecAttrService): keychainService 20 | ] 21 | 22 | /** 23 | * Sets the password for the specified account from the app's keychain, or nil if unavailable 24 | */ 25 | public static func setValue(_ value: String, for userAccount: String, withLabel label: String? = nil) throws { 26 | guard let encodedPassword = value.data(using: .utf8) else { 27 | throw KeychainError.serializationError 28 | } 29 | 30 | var query = queryBase 31 | query[String(kSecAttrAccount)] = userAccount 32 | 33 | if let label = label { 34 | query[String(kSecAttrLabel)] = label 35 | } 36 | 37 | var status = SecItemCopyMatching(query as CFDictionary, nil) 38 | switch status { 39 | case errSecSuccess: 40 | var attributesToUpdate: [String: Any] = [:] 41 | attributesToUpdate[String(kSecValueData)] = encodedPassword 42 | 43 | status = SecItemUpdate(query as CFDictionary, 44 | attributesToUpdate as CFDictionary) 45 | if status != errSecSuccess { 46 | throw KeychainError.from(status: status) 47 | } 48 | case errSecItemNotFound: 49 | query[String(kSecValueData)] = encodedPassword 50 | 51 | status = SecItemAdd(query as CFDictionary, nil) 52 | if status != errSecSuccess { 53 | throw KeychainError.from(status: status) 54 | } 55 | default: 56 | throw KeychainError.from(status: status) 57 | } 58 | } 59 | 60 | /** 61 | * Gets the password for the specified account from the app's keychain, or nil if unavailable 62 | */ 63 | public static func getValue(for userAccount: String) throws -> String? { 64 | var query = queryBase 65 | query[String(kSecAttrAccount)] = userAccount 66 | 67 | query[String(kSecMatchLimit)] = kSecMatchLimitOne 68 | query[String(kSecReturnAttributes)] = kCFBooleanTrue 69 | query[String(kSecReturnData)] = kCFBooleanTrue 70 | 71 | var queryResult: AnyObject? 72 | let status = withUnsafeMutablePointer(to: &queryResult) { 73 | SecItemCopyMatching(query as CFDictionary, $0) 74 | } 75 | 76 | switch status { 77 | case errSecSuccess: 78 | guard 79 | let queriedItem = queryResult as? [String: Any], 80 | let passwordData = queriedItem[String(kSecValueData)] as? Data, 81 | let password = String(data: passwordData, encoding: .utf8) 82 | else { 83 | throw KeychainError.deserializationError 84 | } 85 | return password 86 | case errSecItemNotFound: 87 | return nil 88 | default: 89 | throw KeychainError.from(status: status) 90 | } 91 | } 92 | 93 | /** 94 | * Removes the value for the specified account from the app's keychain 95 | */ 96 | public static func removeValue(for userAccount: String) throws { 97 | var query = queryBase 98 | query[String(kSecAttrAccount)] = userAccount 99 | 100 | let status = SecItemDelete(query as CFDictionary) 101 | guard status == errSecSuccess || status == errSecItemNotFound else { 102 | throw KeychainError.from(status: status) 103 | } 104 | } 105 | 106 | /** 107 | * Removes all values from the app's keychain 108 | */ 109 | public static func removeAllValues() throws { 110 | let status = SecItemDelete(queryBase as CFDictionary) 111 | guard status == errSecSuccess || status == errSecItemNotFound else { 112 | throw KeychainError.from(status: status) 113 | } 114 | } 115 | 116 | ///Gets an NSAlert to notify the user of a Keychain access error 117 | public static func getErrorAlert(_ error: Error) -> NSAlert { 118 | let alert = NSAlert() 119 | alert.alertStyle = .critical 120 | alert.messageText = NSLocalizedString("message.keychain.error", comment: "") 121 | alert.informativeText = error.localizedDescription 122 | return alert 123 | } 124 | } 125 | 126 | public enum KeychainError: Error, LocalizedError { 127 | case serializationError 128 | case deserializationError 129 | case unhandledError(message: String) 130 | 131 | public var errorDescription: String? { 132 | switch self { 133 | case .serializationError: 134 | return "Data serialization error" 135 | case .deserializationError: 136 | return "Data deserialization error" 137 | case .unhandledError(let message): 138 | return message 139 | } 140 | } 141 | 142 | static func from(status: OSStatus) -> KeychainError { 143 | let message = SecCopyErrorMessageString(status, nil) as String? ?? "Unhandled error" 144 | return KeychainError.unhandledError(message: message) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /AirMessage/Helper/LogManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LogHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-24. 6 | // 7 | 8 | import Foundation 9 | import os 10 | import Sentry 11 | 12 | enum LogLevel { 13 | case debug 14 | case info 15 | case notice 16 | case error 17 | case fault 18 | 19 | /** 20 | Gets this log level as a display string 21 | */ 22 | var label: String { 23 | switch self { 24 | case .debug: 25 | return "debug" 26 | case .info: 27 | return "info" 28 | case .notice: 29 | return "notice" 30 | case .error: 31 | return "error" 32 | case .fault: 33 | return "fault" 34 | } 35 | } 36 | 37 | /** 38 | Gets this log level as an `OSLogType` 39 | */ 40 | @available(macOS 10.12, *) 41 | var osLogType: OSLogType { 42 | switch self { 43 | case .debug: 44 | return .debug 45 | case .info: 46 | return .info 47 | case .notice: 48 | return .default 49 | case .error: 50 | return .error 51 | case .fault: 52 | return .fault 53 | } 54 | } 55 | } 56 | 57 | private let standardDateFormatter: DateFormatter = { 58 | let dateFormatter = DateFormatter() 59 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 60 | dateFormatter.dateFormat = "yyyy-MM-dd HH-mm-ss" 61 | return dateFormatter 62 | }() 63 | 64 | private struct FileLogger: TextOutputStream { 65 | let file: URL 66 | 67 | func write(_ string: String) { 68 | let fileRestored: Bool 69 | let fileHandle: FileHandle 70 | do { 71 | fileHandle = try FileHandle(forWritingTo: file) 72 | fileRestored = false 73 | } catch CocoaError.fileNoSuchFile { 74 | //Create the file 75 | guard FileManager.default.createFile(atPath: file.path, contents: nil) else { 76 | print("Failed to recreate log file at \(file.path), exiting") 77 | SentrySDK.capture(message: "Failed to recreate log file at \(file.path)") 78 | exit(1) 79 | } 80 | 81 | //Get a new file handle 82 | fileHandle = try! FileHandle(forWritingTo: file) 83 | fileRestored = true 84 | } catch { 85 | print("Failed to acquire handle to file at \(file.path), exiting") 86 | SentrySDK.capture(error: error) 87 | exit(1) 88 | } 89 | 90 | try! fileHandle.seekToEndCompat() 91 | if fileRestored { 92 | try! fileHandle.writeCompat(contentsOf: "[earlier entries from this log file have been lost]\n".data(using: .utf8)!) 93 | } 94 | try! fileHandle.writeCompat(contentsOf: string.data(using: .utf8)!) 95 | } 96 | } 97 | 98 | class LogManager { 99 | private static var fileLogger: FileLogger = { 100 | //Create the log directory 101 | let logDirectory = StorageManager.storageDirectory.appendingPathComponent("logs", isDirectory: true) 102 | if !FileManager.default.fileExists(atPath: logDirectory.path) { 103 | try! FileManager.default.createDirectory(at: logDirectory, withIntermediateDirectories: false, attributes: nil) 104 | } 105 | 106 | //Move the previous log file 107 | let logFile = logDirectory.appendingPathComponent("latest.log", isDirectory: false) 108 | if FileManager.default.fileExists(atPath: logFile.path) { 109 | let dateFormatter = DateFormatter() 110 | dateFormatter.locale = Locale(identifier: "en_US_POSIX") 111 | dateFormatter.dateFormat = "yyyy-MM-dd HH-mm-ss" 112 | 113 | let targetFileName = dateFormatter.string(from: Date()) + ".log" 114 | let targetFile = logDirectory.appendingPathComponent(targetFileName, isDirectory: false) 115 | 116 | try! FileManager.default.moveItem(at: logFile, to: targetFile) 117 | } 118 | 119 | guard FileManager.default.createFile(atPath: logFile.path, contents: nil) else { 120 | print("Failed to create log file at \(logFile.path), exiting") 121 | SentrySDK.capture(message: "Failed to create log file at \(logFile.path)") 122 | exit(1) 123 | } 124 | 125 | //Create the file logger 126 | return FileLogger(file: logFile) 127 | }() 128 | 129 | @available(macOS 11.0, *) 130 | private static var loggerMain = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "main") 131 | @available(macOS 10.12, *) 132 | private static var osLogMain = OSLog.init(subsystem: Bundle.main.bundleIdentifier!, category: "main") 133 | 134 | public static func log(_ message: String, level: LogLevel) { 135 | let timestamp = standardDateFormatter.string(from: Date()) 136 | let typeLabel = level.label 137 | 138 | //Log to file 139 | fileLogger.write("\(timestamp) [\(typeLabel)] \(message)\n") 140 | 141 | if #available(macOS 11.0, *) { 142 | //Log to Logger 143 | loggerMain.log(level: level.osLogType, "\(message, privacy: .public)") 144 | } else if #available(macOS 10.12, *) { 145 | //Log to os_log 146 | os_log("%{public}@", log: osLogMain, type: level.osLogType, message) 147 | } else { 148 | //Log to standard output 149 | NSLog("[\(typeLabel)] \(message)") 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /AirMessage/Helper/PasswordGrade.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PasswordGrade.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-01-04. 6 | // 7 | 8 | import Foundation 9 | 10 | func calculatePasswordStrength(_ password: String) -> Int { 11 | var score = 0 12 | 13 | //Up to 6 points for a length of 10 14 | score += min(Int(Double(password.count) / 10.0 * 6.0), 6) 15 | 16 | //1 point for a digit 17 | if password.range(of: "(?=.*[0-9]).*", options: .regularExpression) != nil { 18 | score += 1 19 | } 20 | 21 | //1 point for a lowercase letter 22 | if password.range(of: "(?=.*[a-z]).*", options: .regularExpression) != nil { 23 | score += 1 24 | } 25 | 26 | //1 point for an uppercase letter 27 | if password.range(of: "(?=.*[A-Z]).*", options: .regularExpression) != nil { 28 | score += 1 29 | } 30 | 31 | //1 point for a special character 32 | if password.range(of: "(?=.*[~!@#$%^&*()_-]).*", options: .regularExpression) != nil { 33 | score += 1 34 | } 35 | 36 | return score 37 | } 38 | 39 | func getPasswordStrengthLabel(_ value: Int) -> String { 40 | switch value { 41 | case 0..<5: 42 | return NSLocalizedString("label.password_strength.level1", comment: "") 43 | case 5..<7: 44 | return NSLocalizedString("label.password_strength.level2", comment: "") 45 | case 7..<9: 46 | return NSLocalizedString("label.password_strength.level3", comment: "") 47 | default: 48 | return NSLocalizedString("label.password_strength.level4", comment: "") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /AirMessage/Helper/PreferencesManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Cole Feuer on 2021-06-27. 3 | // 4 | 5 | import Foundation 6 | import Sentry 7 | 8 | let defaultServerPort = 1359 9 | 10 | class PreferencesManager { 11 | public static let shared = PreferencesManager() 12 | 13 | private init() {} 14 | 15 | // MARK: UserDefaults 16 | 17 | private enum UDKeys: String, CaseIterable { 18 | //Settings 19 | case serverPort 20 | case checkUpdates 21 | case betaUpdates 22 | case faceTimeIntegration 23 | case accountType 24 | 25 | //Storage 26 | case connectUserID 27 | case connectEmailAddress 28 | } 29 | 30 | ///Registers default preferences values 31 | func registerPreferences() { 32 | UserDefaults.standard.register(defaults: [ 33 | UDKeys.serverPort.rawValue: defaultServerPort, 34 | UDKeys.checkUpdates.rawValue: true, 35 | UDKeys.betaUpdates.rawValue: false, 36 | UDKeys.faceTimeIntegration.rawValue: false, 37 | UDKeys.accountType.rawValue: AccountType.unknown.rawValue 38 | ]) 39 | } 40 | 41 | var serverPort: Int { 42 | get { 43 | UserDefaults.standard.integer(forKey: UDKeys.serverPort.rawValue) 44 | } 45 | set(newValue) { 46 | UserDefaults.standard.set(newValue, forKey: UDKeys.serverPort.rawValue) 47 | } 48 | } 49 | 50 | var checkUpdates: Bool { 51 | get { 52 | UserDefaults.standard.bool(forKey: UDKeys.checkUpdates.rawValue) 53 | } 54 | set(newValue) { 55 | UserDefaults.standard.set(newValue, forKey: UDKeys.checkUpdates.rawValue) 56 | } 57 | } 58 | 59 | var betaUpdates: Bool { 60 | get { 61 | UserDefaults.standard.bool(forKey: UDKeys.betaUpdates.rawValue) 62 | } 63 | set(newValue) { 64 | UserDefaults.standard.set(newValue, forKey: UDKeys.betaUpdates.rawValue) 65 | } 66 | } 67 | 68 | var faceTimeIntegration: Bool { 69 | get { 70 | UserDefaults.standard.bool(forKey: UDKeys.faceTimeIntegration.rawValue) 71 | } 72 | set(newValue) { 73 | UserDefaults.standard.set(newValue, forKey: UDKeys.faceTimeIntegration.rawValue) 74 | } 75 | } 76 | 77 | var accountType: AccountType { 78 | get { 79 | AccountType(rawValue: UserDefaults.standard.integer(forKey: UDKeys.accountType.rawValue)) 80 | ?? AccountType.unknown 81 | } 82 | set(newValue) { 83 | UserDefaults.standard.set(newValue.rawValue, forKey: UDKeys.accountType.rawValue) 84 | } 85 | } 86 | 87 | var connectUserID: String? { 88 | get { 89 | UserDefaults.standard.string(forKey: UDKeys.connectUserID.rawValue) 90 | } 91 | set(newValue) { 92 | UserDefaults.standard.set(newValue, forKey: UDKeys.connectUserID.rawValue) 93 | } 94 | } 95 | 96 | var connectEmailAddress: String? { 97 | get { 98 | UserDefaults.standard.string(forKey: UDKeys.connectEmailAddress.rawValue) 99 | } 100 | set(newValue) { 101 | UserDefaults.standard.set(newValue, forKey: UDKeys.connectEmailAddress.rawValue) 102 | } 103 | } 104 | 105 | // MARK: Keychain 106 | 107 | private enum KeychainAccount: String, CaseIterable { 108 | case password = "airmessage-password" 109 | case installationID = "airmessage-installation" 110 | } 111 | 112 | private var keychainInitialized = false 113 | private var keychainValues = [String: String]() 114 | 115 | func initializeKeychain() throws { 116 | try runOnMain { 117 | guard !keychainInitialized else { return } 118 | 119 | do { 120 | //Load all Keychain accounts into the cache map 121 | for account in KeychainAccount.allCases { 122 | keychainValues[account.rawValue] = try KeychainManager.getValue(for: account.rawValue) 123 | } 124 | 125 | //Generate an installation ID if one isn't present 126 | if keychainValues[KeychainAccount.installationID.rawValue] == nil { 127 | let generatedInstallationID = UUID().uuidString 128 | try setValue(generatedInstallationID, for: KeychainAccount.installationID) 129 | } 130 | 131 | keychainInitialized = true 132 | } catch { 133 | LogManager.log("Failed to load Keychain values: \(error.localizedDescription)", level: .notice) 134 | SentrySDK.capture(error: error) 135 | 136 | //Rethrow error 137 | throw error 138 | } 139 | } 140 | } 141 | 142 | private func setValue(_ value: String, for account: KeychainAccount) throws { 143 | try runOnMain { 144 | if value.isEmpty { 145 | try KeychainManager.removeValue(for: account.rawValue) 146 | } else { 147 | try KeychainManager.setValue(value, for: account.rawValue) 148 | } 149 | keychainValues[account.rawValue] = value 150 | } 151 | } 152 | 153 | var password: String { 154 | runOnMain { 155 | keychainValues[KeychainAccount.password.rawValue] ?? "" 156 | } 157 | } 158 | 159 | func setPassword(_ password: String) throws { 160 | try setValue(password, for: KeychainAccount.password) 161 | } 162 | 163 | var installationID: String { 164 | runOnMain { 165 | keychainValues[KeychainAccount.installationID.rawValue]! 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /AirMessage/Helper/ProcessHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProcessHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2022-08-13. 6 | // 7 | 8 | import Foundation 9 | import Sentry 10 | 11 | ///Starts a process, waits for it to finish, and logs any errors the process encountered while running 12 | func runProcessCatchError(_ process: Process) throws { 13 | //Capture the process' error pipe 14 | let errorPipe = Pipe() 15 | process.standardOutput = nil 16 | process.standardError = errorPipe 17 | 18 | //Start the process 19 | if #available(macOS 10.13, *) { 20 | try process.run() 21 | } else { 22 | process.launch() 23 | } 24 | 25 | //Wait for the process to exit 26 | process.waitUntilExit() 27 | 28 | //Check the output code 29 | guard process.terminationStatus == 0 else { 30 | let errorFileHandle = errorPipe.fileHandleForReading 31 | 32 | let errorMessage: String? 33 | if let data = try? errorFileHandle.readToEndCompat(), !data.isEmpty { 34 | errorMessage = String(data: data, encoding: .utf8) 35 | } else { 36 | errorMessage = nil 37 | } 38 | 39 | //Throw a process error 40 | throw ProcessError(exitCode: process.terminationStatus, message: errorMessage) 41 | } 42 | } 43 | 44 | struct ProcessError: Error { 45 | let exitCode: Int32 46 | let message: String? 47 | 48 | init(exitCode: Int32, message: String?) { 49 | self.exitCode = exitCode 50 | self.message = message 51 | } 52 | 53 | public var localizedDescription: String { 54 | if let message = message { 55 | return "Exit code \(exitCode): \(message)" 56 | } else { 57 | return "Exit code \(exitCode)" 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /AirMessage/Helper/ReadWriteLock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReadWriteLock.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-16. 6 | // 7 | 8 | import Foundation 9 | 10 | class ReadWriteLock { 11 | private var lock: pthread_rwlock_t 12 | 13 | init() { 14 | lock = pthread_rwlock_t() 15 | pthread_rwlock_init(&lock, nil) 16 | } 17 | 18 | deinit { 19 | pthread_rwlock_destroy(&lock) 20 | } 21 | 22 | @discardableResult 23 | public func withReadLock(_ body: () throws -> Result) rethrows -> Result { 24 | pthread_rwlock_rdlock(&lock) 25 | defer { pthread_rwlock_unlock(&lock) } 26 | return try body() 27 | } 28 | 29 | @discardableResult 30 | public func withWriteLock(_ body: () throws -> Return) rethrows -> Return { 31 | pthread_rwlock_wrlock(&lock) 32 | defer { pthread_rwlock_unlock(&lock) } 33 | return try body() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /AirMessage/Helper/ServerLaunch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServerLaunch.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-10. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | /** 12 | Automatically sets the most appropriate data proxy 13 | - Returns: Whether a data proxy was set 14 | */ 15 | func setDataProxyAuto() -> Bool { 16 | switch PreferencesManager.shared.accountType { 17 | case .direct: 18 | ConnectionManager.shared.setProxy(DataProxyTCP(port: PreferencesManager.shared.serverPort)) 19 | return true 20 | case .connect: 21 | guard let userID = PreferencesManager.shared.connectUserID else { 22 | LogManager.log("Couldn't set default data proxy - no Connect user ID", level: .notice) 23 | return false 24 | } 25 | ConnectionManager.shared.setProxy(DataProxyConnect(userID: userID)) 26 | return true 27 | case .unknown: 28 | return false 29 | } 30 | } 31 | 32 | /** 33 | * Checks for launch permissions and starts the server, or restarts it if it's already running 34 | */ 35 | func launchServer() { 36 | //Check for setup and permissions before launching 37 | guard PreferencesManager.shared.accountType != .unknown && checkServerPermissions() else { 38 | NotificationNames.postUpdateUIState(.setup) 39 | return 40 | } 41 | 42 | //Connect to the database 43 | do { 44 | try DatabaseManager.shared.start() 45 | } catch { 46 | LogManager.log("Failed to start database: \(error)", level: .notice) 47 | NotificationNames.postUpdateUIState(.errorDatabase) 48 | return 49 | } 50 | 51 | //Start listening for FaceTime calls 52 | if FaceTimeHelper.isSupported && PreferencesManager.shared.faceTimeIntegration { 53 | FaceTimeHelper.startIncomingCallTimer() 54 | } 55 | 56 | //Start the server 57 | ConnectionManager.shared.start() 58 | } 59 | 60 | /** 61 | * Runs checks to test if the server has all the permissions it needs to work 62 | */ 63 | func checkServerPermissions() -> Bool { 64 | //Check for Messages Automation access 65 | guard AppleScriptBridge.shared.checkPermissionsMessages() else { 66 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 67 | let windowController = storyboard.instantiateController(withIdentifier: "AutomationAccess") as! NSWindowController 68 | (windowController.contentViewController as! AutomationAccessViewController).onDone = launchServer 69 | windowController.showWindow(nil) 70 | 71 | return false 72 | } 73 | 74 | //Check for Accessibility access (for sending messages on macOS 11+ or FaceTime) 75 | if #available(macOS 11.0, *) { 76 | guard AppleScriptBridge.shared.checkPermissionsAutomation() else { 77 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 78 | let windowController = storyboard.instantiateController(withIdentifier: "AccessibilityAccess") as! NSWindowController 79 | (windowController.contentViewController as! AccessibilityAccessViewController).onDone = launchServer 80 | windowController.showWindow(nil) 81 | 82 | return false 83 | } 84 | } 85 | 86 | //Check for Full Disk Access 87 | do { 88 | try FileManager.default.contentsOfDirectory(atPath: NSHomeDirectory() + "/Library/Messages") 89 | } catch { 90 | LogManager.log("Failed to read Messages directory: \(error)", level: .info) 91 | 92 | let storyboard = NSStoryboard(name: "Main", bundle: nil) 93 | let windowController = storyboard.instantiateController(withIdentifier: "FullDiskAccess") as! NSWindowController 94 | windowController.showWindow(nil) 95 | 96 | return false 97 | } 98 | 99 | return true 100 | } 101 | 102 | /** 103 | * Stops the server and resets the state to setup 104 | */ 105 | func resetServer() { 106 | PreferencesManager.shared.accountType = .unknown 107 | NotificationNames.postUpdateSetupMode(true) 108 | 109 | ConnectionManager.shared.stop() 110 | DatabaseManager.shared.stop() 111 | } 112 | -------------------------------------------------------------------------------- /AirMessage/Helper/StorageManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageManager.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-15. 6 | // 7 | 8 | import Foundation 9 | 10 | class StorageManager { 11 | /** 12 | Gets the application storage directory 13 | This call will create the directory if it doesn't exist 14 | */ 15 | static var storageDirectory: URL = { 16 | let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! 17 | let appDir = applicationSupport.appendingPathComponent("AirMessage") 18 | try! FileManager.default.createDirectory(at: appDir, withIntermediateDirectories: true, attributes: nil) 19 | 20 | return appDir 21 | }() 22 | } 23 | -------------------------------------------------------------------------------- /AirMessage/Helper/SystemHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SystemHelper.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-16. 6 | // 7 | 8 | import Foundation 9 | import IOKit.pwr_mgt 10 | import MachO 11 | 12 | private var sleepAssertionID: IOPMAssertionID = 0 13 | 14 | func lockSystemSleep() { 15 | IOPMAssertionCreateWithName(kIOPMAssertionTypeNoIdleSleep as CFString, IOPMAssertionLevel(kIOPMAssertionLevelOn), "AirMessage runs as a background service" as CFString, &sleepAssertionID) 16 | } 17 | 18 | func releaseSystemSleep() { 19 | IOPMAssertionRelease(sleepAssertionID) 20 | } 21 | 22 | /** 23 | Gets the name of the computer 24 | */ 25 | func getComputerName() -> String? { 26 | return Host.current().localizedName 27 | } 28 | -------------------------------------------------------------------------------- /AirMessage/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | CONNECT_ENDPOINT 24 | $(CONNECT_ENDPOINT) 25 | FIREBASE_API_KEY 26 | $(FIREBASE_API_KEY) 27 | GOOGLE_OAUTH_CLIENT_ID 28 | $(GOOGLE_OAUTH_CLIENT_ID) 29 | GOOGLE_OAUTH_CLIENT_SECRET 30 | $(GOOGLE_OAUTH_CLIENT_SECRET) 31 | LSApplicationCategoryType 32 | public.app-category.social-networking 33 | LSMinimumSystemVersion 34 | $(MACOSX_DEPLOYMENT_TARGET) 35 | LSUIElement 36 | 37 | NSAppleEventsUsageDescription 38 | AirMessage needs to control Messages to send outgoing messages 39 | NSMainStoryboardFile 40 | Main 41 | NSPrincipalClass 42 | NSApplication 43 | SENTRY_DSN 44 | $(SENTRY_DSN) 45 | 46 | 47 | -------------------------------------------------------------------------------- /AirMessage/Library/OpenSSL/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /AirMessage/Library/OpenSSL/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "OpenSSL", 8 | pkgConfig: "openssl", 9 | targets: [ 10 | .systemLibrary(name: "openssl", pkgConfig: "openssl", providers: [.brew(["openssl"])]), 11 | .target(name: "OpenSSL", dependencies: ["openssl"]) 12 | ] 13 | ) 14 | -------------------------------------------------------------------------------- /AirMessage/Library/OpenSSL/module.modulemap: -------------------------------------------------------------------------------- 1 | module OpenSSL { 2 | umbrella header "openssl.h" 3 | link "openssl" 4 | } 5 | -------------------------------------------------------------------------------- /AirMessage/Library/OpenSSL/openssl.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | -------------------------------------------------------------------------------- /AirMessage/LocalizeStoryboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizeStoryboard.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-26. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | extension NSTextField { 12 | @IBInspectable var localizedText: String { 13 | set(key) { 14 | stringValue = NSLocalizedString(key, comment: "") 15 | } 16 | 17 | get { 18 | stringValue 19 | } 20 | } 21 | } 22 | 23 | extension NSButton { 24 | @IBInspectable var localizedText: String { 25 | set(key) { 26 | title = NSLocalizedString(key, comment: "") 27 | } 28 | 29 | get { 30 | title 31 | } 32 | } 33 | } 34 | 35 | extension NSMenuItem { 36 | @IBInspectable var localizedText: String { 37 | set(key) { 38 | title = NSLocalizedString(key, comment: "") 39 | } 40 | 41 | get { 42 | title 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /AirMessage/MessageInterop/MessageError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageError.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-20. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | An error that represents an AppleScript execution error 12 | */ 13 | struct AppleScriptError: Error, LocalizedError, CustomNSError { 14 | let errorDict: [String: Any] 15 | let fileURL: URL? 16 | init(error: NSDictionary, fileURL: URL? = nil) { 17 | errorDict = error as! [String: Any] 18 | self.fileURL = fileURL 19 | } 20 | 21 | var code: Int { 22 | (errorDict[NSAppleScript.errorNumber] as? Int) ?? 0 23 | } 24 | var message: String { 25 | (errorDict[NSAppleScript.errorMessage] as? String) ?? "AppleScript execution error" 26 | } 27 | 28 | //LocalizedError 29 | 30 | var errorDescription: String? { 31 | if let fileURL = fileURL { 32 | return "AppleScript error \(code) for file \(fileURL): \(message)" 33 | } else { 34 | return "AppleScript error \(code): \(message)" 35 | } 36 | } 37 | 38 | //CustomNSError 39 | 40 | static let errorDomain = "AppleScriptErrorDomain" 41 | var errorCode: Int { code } 42 | var errorUserInfo: [String: Any] { errorDict } 43 | } 44 | 45 | ///An error that represents a failure to load an AppleScript file 46 | struct AppleScriptInitializationError: Error, LocalizedError { 47 | let fileURL: URL 48 | init(fileURL: URL) { 49 | self.fileURL = fileURL 50 | } 51 | 52 | public var errorDescription: String? { 53 | "Failed to load AppleScript file \(fileURL)" 54 | } 55 | } 56 | 57 | /** 58 | An error that represents when functionality isn't available on a newer version of macOS 59 | */ 60 | struct ForwardsSupportError: Error, LocalizedError { 61 | let noSupportVer: String 62 | init(noSupportVer: String) { 63 | self.noSupportVer = noSupportVer 64 | } 65 | 66 | public var errorDescription: String? { 67 | "Not supported beyond macOS \(noSupportVer) (running \(ProcessInfo.processInfo.operatingSystemVersionString))" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /AirMessage/MessageInterop/MessageManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageManager.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-24. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class MessageManager { 12 | static func createChat(withAddresses addresses: [String], service: String) throws -> String { 13 | if #available(macOS 11.0, *) { 14 | //Not supported 15 | throw ForwardsSupportError(noSupportVer: "11.0") 16 | } else { 17 | return try AppleScriptBridge.shared.createChat(withAddresses: addresses, service: service) 18 | } 19 | } 20 | 21 | static func send(message: String, toExistingChat chatID: String) throws { 22 | return try AppleScriptBridge.shared.sendMessage(toExistingChat: chatID, message: message, isFile: false) 23 | } 24 | 25 | static func send(file: URL, toExistingChat chatID: String) throws { 26 | return try AppleScriptBridge.shared.sendMessage(toExistingChat: chatID, message: file.path, isFile: true) 27 | } 28 | 29 | static func send(message: String, toNewChat addresses: [String], onService service: String) throws { 30 | if #available(macOS 11.0, *) { 31 | if addresses.count == 1 { 32 | //Send the message directly to the user 33 | return try AppleScriptBridge.shared.sendMessage(toDirect: addresses[0], service: service, message: message, isFile: false) 34 | } else { 35 | //NSSharingService only supports iMessage 36 | guard service == "iMessage" else { 37 | throw ForwardsSupportError(noSupportVer: "11.0") 38 | } 39 | 40 | DispatchQueue.main.sync { 41 | //Open the sharing service 42 | let service = NSSharingService(named: NSSharingService.Name.composeMessage)! 43 | service.delegate = autoSubmitNSSharingServiceDelegate 44 | service.recipients = addresses 45 | service.perform(withItems: [message]) 46 | } 47 | } 48 | } else { 49 | try AppleScriptBridge.shared.sendMessage(toNewChat: addresses, service: service, message: message, isFile: false) 50 | } 51 | } 52 | 53 | static func send(file: URL, toNewChat addresses: [String], onService service: String) throws { 54 | if #available(macOS 11.0, *) { 55 | if addresses.count == 1 { 56 | //Send the message directly to the user 57 | return try AppleScriptBridge.shared.sendMessage(toDirect: addresses[0], service: service, message: file.path, isFile: true) 58 | } else { 59 | //NSSharingService only supports iMessage 60 | guard service == "iMessage" else { 61 | throw ForwardsSupportError(noSupportVer: "11.0") 62 | } 63 | 64 | DispatchQueue.main.sync { 65 | //Open the sharing service 66 | let service = NSSharingService(named: NSSharingService.Name.composeMessage)! 67 | service.delegate = autoSubmitNSSharingServiceDelegate 68 | service.recipients = addresses 69 | service.perform(withItems: [file]) 70 | } 71 | } 72 | } else { 73 | return try AppleScriptBridge.shared.sendMessage(toNewChat: addresses, service: service, message: file.path, isFile: true) 74 | } 75 | } 76 | } 77 | 78 | private let autoSubmitNSSharingServiceDelegate = AutoSubmitNSSharingServiceDelegate() 79 | 80 | private class AutoSubmitNSSharingServiceDelegate: NSObject, NSSharingServiceDelegate { 81 | private var timer: Timer? 82 | 83 | private func startTimer() { 84 | //Ignore if the timer already exists 85 | guard timer == nil else { return } 86 | 87 | //Set a timer to try and submit every second 88 | timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(runSubmit), userInfo: nil, repeats: true) 89 | } 90 | 91 | private func stopTimer() { 92 | timer?.invalidate() 93 | timer = nil 94 | } 95 | 96 | func sharingService(_ sharingService: NSSharingService, willShareItems items: [Any]) { 97 | startTimer() 98 | 99 | } 100 | 101 | func sharingService(_ sharingService: NSSharingService, didShareItems: [Any]) { 102 | stopTimer() 103 | } 104 | 105 | func sharingService(_ sharingService: NSSharingService, didFailToShareItems: [Any], error: Error) { 106 | stopTimer() 107 | } 108 | 109 | @objc private func runSubmit() { 110 | do { 111 | try AppleScriptBridge.shared.pressCommandReturn() 112 | } catch { 113 | stopTimer() 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /AirMessage/ObjC/ObjC.h: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC.h 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-16. 6 | // 7 | 8 | #ifndef ObjCException_h 9 | #define ObjCException_h 10 | 11 | #import 12 | 13 | @interface ObjC : NSObject 14 | 15 | + (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error; 16 | 17 | @end 18 | 19 | #endif /* ObjCException_h */ 20 | -------------------------------------------------------------------------------- /AirMessage/ObjC/ObjC.m: -------------------------------------------------------------------------------- 1 | // 2 | // ObjC.m 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-16. 6 | // 7 | 8 | #import "ObjC.h" 9 | 10 | @implementation ObjC 11 | 12 | + (BOOL)catchException:(void(NS_NOESCAPE ^)(void))tryBlock error:(__autoreleasing NSError **)error { 13 | @try { 14 | tryBlock(); 15 | return YES; 16 | } 17 | @catch (NSException *exception) { 18 | *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo]; 19 | return NO; 20 | } 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /AirMessage/Packer/AirPacker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AirPacker.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-14. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A structure for packing and unpacking data across AirMessage clients 12 | */ 13 | struct AirPacker { 14 | private(set) var data: Data 15 | private var currentIndex = 0 16 | 17 | // MARK: - Initialize 18 | 19 | init() { 20 | data = Data() 21 | } 22 | 23 | init(capacity: Int) { 24 | data = Data(capacity: capacity) 25 | } 26 | 27 | init(from source: Data) { 28 | data = source 29 | } 30 | 31 | mutating func reset() { 32 | data = Data() 33 | } 34 | 35 | mutating func reset(capacity: Int) { 36 | data = Data(capacity: capacity) 37 | } 38 | 39 | // MARK: - Write 40 | 41 | private mutating func appendPrimitive(_ value: T) { 42 | withUnsafeBytes(of: value) { ptr in 43 | data.append(contentsOf: ptr) 44 | } 45 | } 46 | 47 | mutating func pack(bool value: Bool) { 48 | data.append(value ? 1 : 0) 49 | } 50 | 51 | mutating func pack(byte value: Int8) { 52 | appendPrimitive(value.bigEndian) 53 | } 54 | 55 | mutating func pack(short value: Int16) { 56 | appendPrimitive(value.bigEndian) 57 | } 58 | 59 | mutating func pack(int value: Int32) { 60 | appendPrimitive(value.bigEndian) 61 | } 62 | 63 | mutating func pack(long value: Int64) { 64 | appendPrimitive(value.bigEndian) 65 | } 66 | 67 | mutating func pack(payload value: Data) { 68 | pack(int: Int32(value.count)) 69 | data.append(value) 70 | } 71 | 72 | mutating func pack(optionalPayload value: Data?) { 73 | if let value = value { 74 | pack(bool: true) 75 | pack(payload: value) 76 | } else { 77 | pack(bool: false) 78 | } 79 | } 80 | 81 | mutating func pack(string value: String) { 82 | pack(payload: value.data(using: .utf8)!) 83 | } 84 | 85 | mutating func pack(optionalString value: String?) { 86 | if let value = value { 87 | pack(bool: true) 88 | pack(string: value) 89 | } else { 90 | pack(bool: false) 91 | } 92 | } 93 | 94 | mutating func pack(arrayHeader value: Int32) { 95 | pack(int: value) 96 | } 97 | 98 | mutating func pack(stringArray value: [String]) { 99 | pack(arrayHeader: Int32(value.count)) 100 | for item in value { 101 | pack(string: item) 102 | } 103 | } 104 | 105 | mutating func pack(packableArray value: [Packable]) { 106 | pack(arrayHeader: Int32(value.count)) 107 | for item in value { 108 | item.pack(to: &self) 109 | } 110 | } 111 | 112 | // MARK: - Read 113 | 114 | /** 115 | Deserializes the data into the primitive at offset 116 | */ 117 | private func deserializePrimitive(fromByteOffset offset: Int, as type: T.Type) -> T { 118 | return data.subdata(in: offset...size) 119 | .withUnsafeBytes { $0.load(as: type) } 120 | } 121 | 122 | mutating func unpackBool() throws -> Bool { 123 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 124 | throw PackingError.rangeError 125 | } 126 | 127 | let value = data[currentIndex] 128 | currentIndex += 1 129 | return value != 0 130 | } 131 | 132 | mutating func unpackByte() throws -> Int8 { 133 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 134 | throw PackingError.rangeError 135 | } 136 | 137 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int8.self) 138 | currentIndex += MemoryLayout.size 139 | return Int8(bigEndian: value) 140 | } 141 | 142 | mutating func unpackShort() throws -> Int16 { 143 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 144 | throw PackingError.rangeError 145 | } 146 | 147 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int16.self) 148 | currentIndex += MemoryLayout.size 149 | return Int16(bigEndian: value) 150 | } 151 | 152 | mutating func unpackInt() throws -> Int32 { 153 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 154 | throw PackingError.rangeError 155 | } 156 | 157 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int32.self) 158 | currentIndex += MemoryLayout.size 159 | return Int32(bigEndian: value) 160 | } 161 | 162 | mutating func unpackLong() throws -> Int64 { 163 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 164 | throw PackingError.rangeError 165 | } 166 | 167 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int64.self) 168 | currentIndex += MemoryLayout.size 169 | return Int64(bigEndian: value) 170 | } 171 | 172 | mutating func unpackPayload() throws -> Data { 173 | let length = Int(try unpackInt()) 174 | 175 | //Protect against large allocations 176 | guard length < CommConst.maxPacketAllocation else { 177 | throw PackingError.allocationError 178 | } 179 | 180 | guard currentIndex + length - 1 < data.count else { 181 | throw PackingError.rangeError 182 | } 183 | 184 | let payload = data.subdata(in: currentIndex.. Data? { 190 | if try unpackBool() { 191 | return try unpackPayload() 192 | } else { 193 | return nil 194 | } 195 | } 196 | 197 | mutating func unpackString() throws -> String { 198 | if let string = String(data: try unpackPayload(), encoding: .utf8) { 199 | return string 200 | } else { 201 | throw PackingError.encodingError 202 | } 203 | } 204 | 205 | mutating func unpackOptionalString() throws -> String? { 206 | if try unpackBool() { 207 | return try unpackString() 208 | } else { 209 | return nil 210 | } 211 | } 212 | 213 | mutating func unpackArrayHeader() throws -> Int32 { 214 | return try unpackInt() 215 | } 216 | 217 | mutating func unpackStringArray() throws -> [String] { 218 | let count = try unpackArrayHeader() 219 | return try (0..(size type: T.Type) { 226 | currentIndex -= MemoryLayout.size 227 | } 228 | } 229 | 230 | protocol Packable { 231 | func pack(to packer: inout AirPacker) 232 | } 233 | -------------------------------------------------------------------------------- /AirMessage/Packer/BytePacker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BytePacker.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-25. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | A structure for packing and unpacking raw data, similar to ByteBuffer 12 | */ 13 | struct BytePacker { 14 | private(set) var data: Data 15 | private(set) var currentIndex = 0 16 | public var count: Int { data.count } 17 | public var remaining: Int { data.count - currentIndex } 18 | 19 | // MARK: - Initialize 20 | 21 | init() { 22 | data = Data() 23 | } 24 | 25 | init(capacity: Int) { 26 | data = Data(capacity: capacity) 27 | } 28 | 29 | init(from source: Data) { 30 | data = source 31 | } 32 | 33 | mutating func reset() { 34 | data = Data() 35 | } 36 | 37 | mutating func reset(capacity: Int) { 38 | data = Data(capacity: capacity) 39 | } 40 | 41 | // MARK: - Write 42 | 43 | private mutating func appendPrimitive(_ value: T) { 44 | withUnsafeBytes(of: value) { ptr in 45 | data.append(contentsOf: ptr) 46 | } 47 | } 48 | 49 | mutating func pack(bool value: Bool) { 50 | data.append(value ? 1 : 0) 51 | } 52 | 53 | mutating func pack(byte value: Int8) { 54 | appendPrimitive(value.bigEndian) 55 | } 56 | 57 | mutating func pack(short value: Int16) { 58 | appendPrimitive(value.bigEndian) 59 | } 60 | 61 | mutating func pack(int value: Int32) { 62 | appendPrimitive(value.bigEndian) 63 | } 64 | 65 | mutating func pack(long value: Int64) { 66 | appendPrimitive(value.bigEndian) 67 | } 68 | 69 | mutating func pack(data value: Data) { 70 | data.append(value) 71 | } 72 | 73 | // MARK: - Read 74 | 75 | /** 76 | Deserializes the data into the primitive at offset 77 | */ 78 | private func deserializePrimitive(fromByteOffset offset: Int, as type: T.Type) -> T { 79 | return data.subdata(in: offset...size) 80 | .withUnsafeBytes { $0.load(as: type) } 81 | } 82 | 83 | mutating func unpackBool() throws -> Bool { 84 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 85 | throw PackingError.rangeError 86 | } 87 | 88 | let value = data[currentIndex] 89 | currentIndex += 1 90 | return value != 0 91 | } 92 | 93 | mutating func unpackByte() throws -> Int8 { 94 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 95 | throw PackingError.rangeError 96 | } 97 | 98 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int8.self) 99 | currentIndex += MemoryLayout.size 100 | return Int8(bigEndian: value) 101 | } 102 | 103 | mutating func unpackShort() throws -> Int16 { 104 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 105 | throw PackingError.rangeError 106 | } 107 | 108 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int16.self) 109 | currentIndex += MemoryLayout.size 110 | return Int16(bigEndian: value) 111 | } 112 | 113 | mutating func unpackInt() throws -> Int32 { 114 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 115 | throw PackingError.rangeError 116 | } 117 | 118 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int32.self) 119 | currentIndex += MemoryLayout.size 120 | return Int32(bigEndian: value) 121 | } 122 | 123 | mutating func unpackLong() throws -> Int64 { 124 | guard currentIndex + MemoryLayout.size - 1 < data.count else { 125 | throw PackingError.rangeError 126 | } 127 | 128 | let value = deserializePrimitive(fromByteOffset: currentIndex, as: Int64.self) 129 | currentIndex += MemoryLayout.size 130 | return Int64(bigEndian: value) 131 | } 132 | 133 | mutating func unpackData(length: Int? = nil) throws -> Data { 134 | if let length = length { 135 | //Unpack data of length 136 | guard currentIndex + length - 1 < data.count else { 137 | throw PackingError.rangeError 138 | } 139 | 140 | let payload = data.subdata(in: currentIndex..(size type: T.Type) { 159 | currentIndex -= MemoryLayout.size 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /AirMessage/Packer/PackingError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PackingError.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-25. 6 | // 7 | 8 | import Foundation 9 | 10 | enum PackingError: Error { 11 | case rangeError 12 | case encodingError 13 | case allocationError 14 | } 15 | -------------------------------------------------------------------------------- /AirMessage/Secrets.default.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Secrets.xcconfig 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-18. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | FIREBASE_API_KEY=AIzaSyDE2nDAKL6smwPmZIBy1IP8-x_pTOqpzfM 12 | 13 | GOOGLE_OAUTH_CLIENT_ID=526640769548-rhj6tlb3bqulf65v81bh4ud42riufsa9.apps.googleusercontent.com 14 | GOOGLE_OAUTH_CLIENT_SECRET=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 15 | 16 | CONNECT_ENDPOINT=wss:/$()/connect-open.airmessage.org 17 | 18 | SENTRY_DSN= 19 | 20 | SENTRY_ORG= 21 | SENTRY_PROJECT= 22 | SENTRY_AUTH_TOKEN= 23 | -------------------------------------------------------------------------------- /AirMessage/Security/CertificateTrust.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CertificateTrust.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-30. 6 | // 7 | 8 | import Foundation 9 | 10 | class CertificateTrust { 11 | ///An array of all certificate files 12 | static var certificateFiles: [URL] = { 13 | let certificatesDir = Bundle.main.resourceURL!.appendingPathComponent("Certificates", isDirectory: true) 14 | return try! FileManager.default.contentsOfDirectory(at: certificatesDir, includingPropertiesForKeys: nil) 15 | }() 16 | 17 | ///All locally-stored certificates 18 | static var secCertificates: [SecCertificate] = { 19 | return certificateFiles.map { fileURL in 20 | SecCertificateCreateWithData(nil, try! Data(contentsOf: fileURL) as CFData)! 21 | } 22 | }() 23 | 24 | ///Evaluates the trust against the root certificates 25 | static func evaluateCertificate(allowing rootCertificates: [SecCertificate], for trust: SecTrust) -> Bool { 26 | //Apply our custom root to the trust object. 27 | var err = SecTrustSetAnchorCertificates(trust, rootCertificates as CFArray) 28 | guard err == errSecSuccess else { return false } 29 | 30 | //Re-enable the system's built-in root certificates. 31 | err = SecTrustSetAnchorCertificatesOnly(trust, false) 32 | guard err == errSecSuccess else { return false } 33 | 34 | //Run a trust evaluation and only allow the connection if it succeeds. 35 | var trustResult: SecTrustResultType = .invalid 36 | err = SecTrustEvaluate(trust, &trustResult) 37 | guard err == errSecSuccess else { return false } 38 | return [.proceed, .unspecified].contains(trustResult) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /AirMessage/Security/Certificates/DigiCertGlobalRootCA.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Security/Certificates/DigiCertGlobalRootCA.crt -------------------------------------------------------------------------------- /AirMessage/Security/Certificates/isrg-root-x1-cross-signed.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/AirMessage/Security/Certificates/isrg-root-x1-cross-signed.der -------------------------------------------------------------------------------- /AirMessage/Security/ForwardCompatURLSessionDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForwardCompatURLSessionDelegate.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-11-30. 6 | // 7 | 8 | import Foundation 9 | 10 | //https://developer.apple.com/forums/thread/77694?answerId=229390022#229390022 11 | 12 | /** 13 | A URL session delegate that accepts more modern root certificates. 14 | This is required, since older Mac computers will not trust these certificates by default. 15 | */ 16 | class ForwardCompatURLSessionDelegate: NSObject, URLSessionDelegate { 17 | public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 18 | #if DEBUG 19 | LogManager.log("Evaluating host \(challenge.protectionSpace.host) for \(challenge.protectionSpace.authenticationMethod)", level: .debug) 20 | #endif 21 | 22 | if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { 23 | //We override server trust evaluation (`NSURLAuthenticationMethodServerTrust`) to allow the 24 | //server to use a custom root certificate (`isrgrootx1.der`). 25 | let trust = challenge.protectionSpace.serverTrust! 26 | if CertificateTrust.evaluateCertificate(allowing: CertificateTrust.secCertificates, for: trust) { 27 | completionHandler(.useCredential, URLCredential(trust: trust)) 28 | } else { 29 | completionHandler(.cancelAuthenticationChallenge, nil) 30 | } 31 | } else { 32 | completionHandler(.performDefaultHandling, nil) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /AirMessage/Security/URLSessionCompat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionCompat.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-12-01. 6 | // 7 | 8 | import Foundation 9 | 10 | class URLSessionCompat { 11 | static let delegate = ForwardCompatURLSessionDelegate() 12 | static let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) 13 | } 14 | 15 | extension URLSession { 16 | /** 17 | A singleton `URLSession` with compatibility for older computers 18 | */ 19 | static var sharedCompat: URLSession { URLSessionCompat.session } 20 | } 21 | -------------------------------------------------------------------------------- /AirMessage/SoftwareUpdate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # SoftwareUpdate.sh 4 | # AirMessage 5 | # 6 | # Created by Cole Feuer on 2021-10-10. 7 | # 8 | 9 | pid="$0" 10 | srcFile="$1" 11 | dstFile="$2" 12 | 13 | #Wait for app to exit 14 | while kill -0 "$pid"; do 15 | sleep 0.5 16 | done 17 | 18 | #Delete old AirMessage installation 19 | rm -rf "$dstFile" 20 | 21 | #Move the new app to the target directory 22 | mv "$srcFile" "$dstFile" 23 | 24 | #Remove the source directory 25 | rm -r "$(dirname "$srcFile")" 26 | 27 | #Wait for app to be registered 28 | sleep 1 29 | 30 | #Open the new app 31 | for i in 1 2 3 4 5 32 | do 33 | open "$dstFile" && break || sleep 1 34 | done 35 | -------------------------------------------------------------------------------- /AirMessage/Views/ClientTableCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientTableCellView.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-08-01. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class ClientTableCellView: NSView { 12 | @IBOutlet weak var icon: NSImageView! 13 | @IBOutlet weak var title: NSTextField! 14 | @IBOutlet weak var subtitle: NSTextField! 15 | } 16 | -------------------------------------------------------------------------------- /AirMessage/Views/DraggableAppView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DraggableAppView.swift 3 | // AirMessage 4 | // 5 | // Created by Cole Feuer on 2021-07-10. 6 | // 7 | 8 | import Foundation 9 | import AppKit 10 | 11 | class DraggableAppView: NSImageView { 12 | override func mouseDown(with event: NSEvent) { 13 | guard #available(macOS 10.13, *) else { return } 14 | 15 | let url = Bundle.main.resourceURL! 16 | let path = url.deletingLastPathComponent().deletingLastPathComponent().absoluteString 17 | 18 | let pasteboardItem = NSPasteboardItem() 19 | pasteboardItem.setString(path, forType: .fileURL) 20 | 21 | let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem) 22 | draggingItem.setDraggingFrame(bounds, contents: Bundle.main.image(forResource: "AppIconResource")!) 23 | 24 | beginDraggingSession(with: [draggingItem], event: event, source: self) 25 | } 26 | } 27 | 28 | extension DraggableAppView: NSDraggingSource { 29 | func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { 30 | .copy 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AirMessage/en.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | message.status.connected_count 6 | 7 | NSStringLocalizedFormatKey 8 | %#@clientCount@ Connected 9 | clientCount 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | zero 16 | No Clients 17 | one 18 | 1 Client 19 | other 20 | %d Clients 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /AirMessage/fr.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | message.status.connected_count 6 | 7 | NSStringLocalizedFormatKey 8 | %#@clientCount@ 9 | clientCount 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | zero 16 | Aucun client n'est connecté 17 | one 18 | 1 client est connecté 19 | other 20 | %d clients sont connectés 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /AirMessage/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | AirMessage 4 | 5 | Created by Cole Feuer on 2021-01-04. 6 | 7 | */ 8 | 9 | "action.ok"="OK"; 10 | "action.done"="完了"; 11 | "action.retry"="再試行"; 12 | "action.cancel"="キャンセル"; 13 | "action.reauthenticate"="再度ログイン"; 14 | "action.sign_out"="ログアウト"; 15 | "action.preferences"="環境設定…"; 16 | "action.switch_to_account"="アカウントに切り替える"; 17 | "action.receive_beta_updates"="ベータ版のアップデートを受け取る"; 18 | "action.connect_account"="アカウントで接続 (推奨)"; 19 | "action.configure_manually"="手動設定を使用"; 20 | "action.open_full_disk_access"="フルディスクアクセスの設定を開く"; 21 | "action.open_automation_settings"="オートメーションの設定を開く"; 22 | "action.open_accessibility_settings"="アクセシビリティの設定を開く"; 23 | "action.edit_password"="パスワードの編集…"; 24 | "action.auto_update"="アップデートを自動的に確認する"; 25 | "action.beta_update"="ベータ版のアップデートを受け取る"; 26 | "action.facetime_integration"="FaceTimeとの連携(実験的)"; 27 | "action.remind_me_later"="後で通知"; 28 | "action.install_update"="アップデートをインストール"; 29 | "action.check_for_updates"="アップデートの確認…"; 30 | "action.about_airmessage"="AirMessageについて"; 31 | "action.quit_airmessage"="AirMessageを終了"; 32 | 33 | "label.password"="パスワード:"; 34 | "label.show_password"="パスワードの表示"; 35 | 36 | "label.password_strength"="パスワード強度: %@"; 37 | "label.password_strength.level1"="弱い"; 38 | "label.password_strength.level2"="普通"; 39 | "label.password_strength.level3"="高い"; 40 | "label.password_strength.level4"="非常に高い"; 41 | 42 | "label.full_disk_access"="フルディスクアクセス"; 43 | "label.automation_access"="オートメーション"; 44 | "label.accessibility_access"="アクセシビリティ"; 45 | 46 | "label.preferences"="環境設定"; 47 | "label.preferences.server_port"="サーバーポート:"; 48 | "label.preferences.security"="セキュリティ:"; 49 | "label.preferences.updates"="アップデート:"; 50 | "label.preferences.facetime"="FaceTime:"; 51 | "label.preferences.account"="アカウント:"; 52 | 53 | "label.software_update"="ソフトウェア・アップデート"; 54 | 55 | "label.client_log"="デバイスログ"; 56 | "label.currently_connected"="現在接続中"; 57 | 58 | "message.intro.title"="AirMessage Server へようこそ!"; 59 | "message.intro.body"="AirMessage Server はメニューバーにあります。アイコンをクリックして、サーバーの状態や設定を確認します。開始するには、サーバーの設定方法を選択してください。"; 60 | 61 | "message.keychain.error"="キーチェーンからの読み込みに失敗しました"; 62 | 63 | "message.grant.full_disk_access"="AirMessageへのフルディスクアクセスの許可"; 64 | "message.grant.full_disk_access.explanation"="AirMessage Server はメッセージを読むために、フルディスクアクセスが必要です。"; 65 | "message.grant.full_disk_access.step1"="• ウィンドウの左下にあるロックをクリックします"; 66 | "message.grant.full_disk_access.step2Yosemite"="• 「フルディスクアクセス」リストに AirMessage を追加します"; 67 | "message.grant.full_disk_access.step2HighSierra"="• アイコンを「フルディスクアクセス」リストにドラッグ&ドロップします"; 68 | "message.grant.full_disk_access.step3"="• AirMessage を再起動します"; 69 | 70 | "message.grant.automation_access"="AirMessage へのメッセージのオートメーションの許可"; 71 | "message.grant.automation_access.explanation"="AirMessage Server がメッセージを送信するためには、メッセージに対するオートメーション権限が必要です。"; 72 | "message.grant.automation_access.instructions"="オートメーション」リストの中から「AirMessage」を見つけ、「メッセージ」のチェックボックスをクリックします"; 73 | 74 | "message.grant.accessibility_access"="AirMessage へアクセシビリティの許可"; 75 | "message.grant.accessibility_access.explanation"="AirMessage Server は、メッセージの送信と FaceTime 通話のためにアクセシビリティ権限が必要です。"; 76 | "message.grant.accessibility_access.step1"="• ウィンドウの左下にあるロックをクリックします"; 77 | "message.grant.accessibility_access.step2"="• AirMessage をアクセシビリティリストに追加します"; 78 | 79 | "message.grant.upgrade_warning"="古いバージョンからアップグレードする場合は、AirMessage をリストから完全に削除してから、再度追加する必要があるあるかもしれません。"; 80 | 81 | "message.status.setup"="セットアップを待っています"; 82 | "message.status.starting"="サーバー起動中…"; 83 | "message.status.connecting"="接続中…"; 84 | "message.status.running"="サーバー実行中"; 85 | "message.status.stopped"="サーバー停止"; 86 | 87 | "message.status.error.database"="データベースへの接続ができません"; 88 | "message.status.error.internal"="内部エラーが発生しました"; 89 | "message.status.error.external"="外部エラーが発生しました"; 90 | "message.status.error.internet"="インターネットに接続できません"; 91 | "message.status.error.keychain"="キーチェーンにアクセスできませんでした"; 92 | "message.status.error.port_unavailable"="ポートが既に使用されている"; 93 | "message.status.error.port_error"="ポートの準備が失敗しました"; 94 | "message.status.error.bad_request"="通信エラーが発生しました"; 95 | "message.status.error.outdated"="アプリは古くなっています"; 96 | "message.status.error.account_validation"="接続されたアカウントは使用できません"; 97 | "message.status.error.token_refresh"="デバイスが登録されていません"; 98 | "message.status.error.no_activation"="アカウントが有効化されていません"; 99 | "message.status.error.account_conflict"="別の場所からログインしています"; 100 | 101 | "message.preference.account_manual"="このサーバーは手動でアクセスできるように設定されています。このコンピューターのインターネットアドレスを使って、AndroidのAirMessageを設定します。"; 102 | "message.preference.account_connect"="このサーバーは %@ 用に設定されています。スマートフォン、タブレット、PC で AirMessage にサインインすると、メッセージのやり取りが始まります。"; 103 | "message.enter_server_port"="サーバーポートを入力してください"; 104 | "message.invalid_server_port"="%@ はサーバーポートとしては使用できません"; 105 | "message.facetime_integration"="Android とウェブで FaceTime 通話を発信および受信"; 106 | 107 | "message.reset.title.direct"="AirMessage Server を再設定しますか?"; 108 | "message.reset.title.connect"="AirMessage Server からログアウトしますか?"; 109 | "message.reset.subtitle"="AirMessage Server を再設定するまでは、AirMessage デバイスでメッセージを受信することはできません。"; 110 | 111 | "message.beta_enrollment.title"="ベータ版のアップデートを受け取りますか?"; 112 | "message.beta_enrollment.description"="ベータ版のアップデートは不安定な場合があり、Android 版 AirMessage のベータ版プログラムに登録する必要があります"; 113 | 114 | "message.beta_unenrollment.title"="ベータ版のアップデートが受けなくなります"; 115 | "message.beta_unenrollment.description"="AirMessage のベータ版を使用している可能性があります"; 116 | 117 | "message.update.title"="新しいバージョンの AirMessage が入手できます!"; 118 | "message.update.available"="AirMessage Server %1$@ が入手できます(使用中のバージョンは%2$@です)。今すぐインストールしますか?"; 119 | "message.update.release_notes"="リリースノート:"; 120 | "message.update.uptodate.title"="新版を使用しています!"; 121 | "message.update.uptodate.description"="AirMessage Server %1$@ は現在入手できる最新バージョンです"; 122 | "message.update.error.title"="アップデートの確認中にエラーが発生しました"; 123 | "message.update.error.parse"="アップデートの詳細をダウンロードできませんでした、後でもう一度お試しください。"; 124 | "message.update.error.download"="アップデートファイルのダウンロードができない"; 125 | "message.update.error.invalid_package"="無効なアップデートパッケージを受信しました"; 126 | "message.update.error.internal"="このアップデートの処理中に内部エラーが発生しました"; 127 | "message.update.error.readonly_volume"="AirMessage をアプリケーションのフォルダに移動して、もう一度お試しください"; 128 | "message.update.error.os_compat"="macOS %d.%d.%d にアップデートして再度お試しください。"; 129 | 130 | "message.register.error.title"="このコンピュータの登録に失敗しました"; 131 | "message.register.error.sign_in"="ログインしようとしてエラーが発生しました"; 132 | 133 | "message.register.success.title"="コンピュータが登録されました"; 134 | "message.register.success.description"="メッセージを受信するには、同じアカウントで携帯電話またはコンピューターで AirMessage にログインします"; 135 | 136 | "progress.check_connection"="接続を確認中…"; 137 | "progress.install_update"="アップデートをインストール中…"; 138 | -------------------------------------------------------------------------------- /AirMessage/ja.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | message.status.connected_count 6 | 7 | NSStringLocalizedFormatKey 8 | %#@clientCount@ 9 | clientCount 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | d 15 | zero 16 | クライアント接続なし 17 | other 18 | %dつのクライアント接続 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /AirMessageTests/CompressionHelperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressionHelperTests.swift 3 | // AirMessageTests 4 | // 5 | // Created by Cole Feuer on 2022-04-15. 6 | // 7 | 8 | import XCTest 9 | @testable import AirMessage 10 | 11 | class CompressionHelperTests: XCTestCase { 12 | func testCompressDecompress() throws { 13 | //Pseudorandom data 14 | var rng = Xorshift128Plus() 15 | var originalData = Data(repeating: 0, count: 1024 * 1024) 16 | for i in originalData.indices { 17 | originalData[i] = UInt8.random(in: UInt8.min...UInt8.max, using: &rng) 18 | } 19 | 20 | //Deflate and inflate the data 21 | let deflatePipe = try CompressionPipeDeflate() 22 | var compressedData = try deflatePipe.pipe(data: &originalData, isLast: true) 23 | 24 | let inflatePipe = try CompressionPipeInflate() 25 | let decompressedData = try inflatePipe.pipe(data: &compressedData) 26 | 27 | //Make sure the data is the same 28 | XCTAssertEqual(originalData, decompressedData, "Original data wasn't the same as decompressed data") 29 | } 30 | 31 | } 32 | 33 | struct Xorshift128Plus: RandomNumberGenerator { 34 | private var xS: UInt64 35 | private var yS: UInt64 36 | 37 | /// Two seeds, `x` and `y`, are required for the random number generator (default values are provided for both). 38 | init(xSeed: UInt64 = 0, ySeed: UInt64 = UInt64.max) { 39 | xS = xSeed == 0 && ySeed == 0 ? UInt64.max : xSeed // Seed cannot be all zeros. 40 | yS = ySeed 41 | } 42 | 43 | mutating func next() -> UInt64 { 44 | var x = xS 45 | let y = yS 46 | xS = y 47 | x ^= x << 23 // a 48 | yS = x ^ y ^ (x >> 17) ^ (y >> 26) // b, c 49 | return yS &+ y 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /AirMessageTests/ContentTypeHelperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentTypeHelperTests.swift 3 | // AirMessageTests 4 | // 5 | // Created by Cole Feuer on 2022-04-15. 6 | // 7 | 8 | import XCTest 9 | @testable import AirMessage 10 | 11 | class ContentTypeHelperTests: XCTestCase { 12 | func testCompareDifferent() { 13 | XCTAssertFalse(compareMIMETypes("image/png", "video/mp4")) 14 | XCTAssertFalse(compareMIMETypes("image/png", "image/jpeg")) 15 | } 16 | 17 | func testCompareEqual() { 18 | XCTAssertTrue(compareMIMETypes("image/png", "image/png")) 19 | XCTAssertTrue(compareMIMETypes("image/*", "image/*")) 20 | } 21 | 22 | func testCompareWildcard() { 23 | XCTAssertTrue(compareMIMETypes("*/*", "*/*")) 24 | XCTAssertTrue(compareMIMETypes("*/*", "image/png")) 25 | XCTAssertTrue(compareMIMETypes("image/png", "*/*")) 26 | XCTAssertTrue(compareMIMETypes("*/*", "image/*")) 27 | } 28 | 29 | func testInvalid() { 30 | XCTAssertFalse(compareMIMETypes("imagepng", "imagepng")) 31 | XCTAssertFalse(compareMIMETypes("**", "**")) 32 | XCTAssertFalse(compareMIMETypes("image/png", "imagepng")) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /OpenSSL/.gitignore: -------------------------------------------------------------------------------- 1 | libcrypto.a 2 | Headers/ 3 | -------------------------------------------------------------------------------- /OpenSSL/Configure.command: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Configure.sh 4 | # AirMessage 5 | # 6 | # Created by Cole Feuer on 2021-11-27. 7 | # 8 | 9 | cd "$(dirname "$0")" 10 | 11 | OPENSSL_VERSION=3.0.5 12 | 13 | #Clean up 14 | rm -rf Headers libcrypto.a 15 | 16 | #Download OpenSSL 17 | echo "Downloading OpenSSL version $OPENSSL_VERSION..." 18 | curl https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz --output openssl-$OPENSSL_VERSION.tar.gz --silent 19 | tar -xf openssl-$OPENSSL_VERSION.tar.gz 20 | pushd openssl-$OPENSSL_VERSION 21 | 22 | #Build for Intel 23 | echo "Building OpenSSL $OPENSSL_VERSION for Intel..." 24 | export MACOSX_DEPLOYMENT_TARGET=10.10 25 | ./Configure darwin64-x86_64 no-deprecated no-shared 26 | make 27 | mv libcrypto.a ../libcrypto-x86_64.a 28 | make clean 29 | 30 | #Build for Apple Silicon 31 | echo "Building OpenSSL $OPENSSL_VERSION for Apple Silicon..." 32 | export MACOSX_DEPLOYMENT_TARGET=11.0 33 | ./Configure darwin64-arm64 no-deprecated no-shared 34 | make 35 | mv libcrypto.a ../libcrypto-arm64.a 36 | 37 | popd 38 | 39 | echo "Finalizing OpenSSL $OPENSSL_VERSION..." 40 | 41 | #Combine libraries 42 | lipo -create -output libcrypto.a libcrypto-x86_64.a libcrypto-arm64.a 43 | 44 | #Copy headers 45 | mkdir -p Headers 46 | cp -r openssl-$OPENSSL_VERSION/include/openssl/. Headers/ 47 | 48 | #Fix inttypes.h 49 | find Headers -type f -name "*.h" -exec sed -i "" -e "s/include /include /g" {} \; 50 | 51 | #Clean up 52 | rm libcrypto-x86_64.a libcrypto-arm64.a openssl-$OPENSSL_VERSION.tar.gz 53 | rm -r openssl-$OPENSSL_VERSION 54 | 55 | echo "Successfully built OpenSSL $OPENSSL_VERSION" 56 | -------------------------------------------------------------------------------- /OpenSSL/OpenSSL.h: -------------------------------------------------------------------------------- 1 | // 2 | // OpenSSL.h 3 | // OpenSSL 4 | // 5 | // Created by Cole Feuer on 2021-11-27. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for OpenSSL. 11 | FOUNDATION_EXPORT double OpenSSLVersionNumber; 12 | 13 | //! Project version string for OpenSSL. 14 | FOUNDATION_EXPORT const unsigned char OpenSSLVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | #include 19 | #include 20 | #include 21 | -------------------------------------------------------------------------------- /OpenSSL/References.txt: -------------------------------------------------------------------------------- 1 | https://github.com/OuterCorner/OpenSSL 2 | https://github.com/krzyzanowskim/OpenSSL 3 | -------------------------------------------------------------------------------- /OpenSSL/module.modulemap: -------------------------------------------------------------------------------- 1 | module OpenSSL [system][extern_c] { 2 | header "OpenSSL.h" 3 | link "libcrypto.a" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AirMessage Server 2 | 3 | ![AirMessage for Android and AirMessage for web connected to AirMessage Server](README/overview.png) 4 | 5 | AirMessage lets people use iMessage on the devices they like. 6 | **AirMessage Server** functions as the bridge between AirMessage client apps and iMessage by running as a service on a Mac computer. 7 | 8 | Other AirMessage repositories: 9 | [Android](https://github.com/airmessage/airmessage-android) | 10 | [Web](https://github.com/airmessage/airmessage-web) | 11 | [Connect (community)](https://github.com/airmessage/airmessage-connect-java) 12 | 13 | ## Getting started 14 | 15 | As AirMessage Server runs on OS X 10.10, this repository targets Xcode 13. To compile with Xcode 14, change the deployment target to macOS 10.13. 16 | 17 | To generate a universal OpenSSL binary to link, run `Configure.command` in the `OpenSSL` directory. 18 | 19 | AirMessage Server uses a configuration file to associate with online services like Firebase and Sentry. 20 | The app will not build without valid configuration files, so to get started quickly, you can copy the provided default file to use a pre-configured Firebase project, or you may provide your own Firebase configuration file: 21 | - `AirMessage/Secrets.default.xcconfig` > `AirMessage/Secrets.xcconfig` 22 | 23 | ## Building and running for AirMessage Connect 24 | 25 | In order to help developers get started quickly, we host a separate open-source version of AirMessage Connect at `connect-open.airmessage.org`. 26 | The default configuration is pre-configured to authenticate and connect to this server. 27 | Since this version of AirMessage Connect is hosted in a separate environment from official servers, you will have to connect client apps to the same AirMessage Connect server. 28 | 29 | We kindly ask that you do not use AirMessage's official Connect servers with any unofficial builds of AirMessage-compatible software. 30 | 31 | --- 32 | 33 | Thank you for your interest in contributing to AirMessage! 34 | You're helping to shape the future of an open, secure messaging market. 35 | Should you have any questions, comments, or concerns, please shoot an email to [hello@airmessage.org](mailto:hello@airmessage.org). 36 | -------------------------------------------------------------------------------- /README/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airmessage/airmessage-server/0cb840029046661da408814e1e1189575919177a/README/overview.png -------------------------------------------------------------------------------- /Zlib/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Zlib/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Zlib", 8 | products: [ 9 | .library(name: "Zlib", targets: ["Zlib"]), 10 | ], 11 | targets: [ 12 | .systemLibrary(name: "Zlib", pkgConfig: "zlib"), 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /Zlib/Sources/Zlib/module.modulemap: -------------------------------------------------------------------------------- 1 | module Zlib [system] { 2 | header "shim.h" 3 | link "zlib" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Zlib/Sources/Zlib/shim.h: -------------------------------------------------------------------------------- 1 | #ifndef ZlibHelpers_h 2 | #define ZlibHelpers_h 3 | 4 | #include 5 | 6 | /** 7 | * Helper function for initializing a zlib deflate stream 8 | * @param stream A pointer to the stream to initialize 9 | * @return The return code of deflateInit 10 | */ 11 | static inline int zlibInitializeDeflate(z_stream *stream) { 12 | stream->zalloc = Z_NULL; 13 | stream->zfree = Z_NULL; 14 | stream->opaque = Z_NULL; 15 | 16 | return deflateInit(stream, Z_DEFAULT_COMPRESSION); 17 | } 18 | 19 | /** 20 | * Helper function for initializing a zlib inflate stream 21 | * @param stream A pointer to the stream to initialize 22 | * @return The return code of inflateInit 23 | */ 24 | static inline int zlibInitializeInflate(z_stream *stream) { 25 | stream->zalloc = Z_NULL; 26 | stream->zfree = Z_NULL; 27 | stream->opaque = Z_NULL; 28 | stream->avail_in = 0; 29 | stream->next_in = Z_NULL; 30 | 31 | return inflateInit(stream); 32 | } 33 | 34 | #endif /* ZlibHelpers_h */ 35 | --------------------------------------------------------------------------------