├── .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 |
4 |
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 |
9 |
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 | 
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 |
--------------------------------------------------------------------------------