From 9001289801aef99492c7d7c84281480dd9a075b0 Mon Sep 17 00:00:00 2001 From: CMK Date: Sun, 25 Apr 2021 12:48:29 +0800 Subject: [PATCH] feat: add push notification --- Mastodon.xcodeproj/project.pbxproj | 241 +++++++++++++++++- .../xcschemes/xcschememanagement.plist | 13 +- .../xcshareddata/swiftpm/Package.resolved | 9 + .../Extension/Array+removeDuplicates.swift | 23 -- Mastodon/Extension/Array.swift | 99 +++++++ Mastodon/Mastodon.entitlements | 2 + .../Settings/SettingsViewController.swift | 14 +- .../Scene/Settings/SettingsViewModel.swift | 101 ++++---- .../APIService/APIService+Subscriptions.swift | 17 +- Mastodon/Service/NotificationService.swift | 241 ++++++++++++++++++ Mastodon/State/AppContext.swift | 8 +- Mastodon/Supporting Files/AppDelegate.swift | 16 +- Mastodon/Supporting Files/AppSecret.swift | 51 ++++ .../MastodonSDK/API/Mastodon+API+Push.swift | 149 +++++------ .../Sources/MastodonSDK/Extension/Data.swift | 9 + NotificationService/Extension/String.swift | 31 +++ NotificationService/Info.plist | 31 +++ .../NotificationService+Decrypt.swift | 96 +++++++ NotificationService/NotificationService.swift | 103 ++++++++ Podfile | 16 ++ Podfile.lock | 9 +- README.md | 1 + 22 files changed, 1094 insertions(+), 186 deletions(-) delete mode 100644 Mastodon/Extension/Array+removeDuplicates.swift create mode 100644 Mastodon/Extension/Array.swift create mode 100644 Mastodon/Service/NotificationService.swift create mode 100644 Mastodon/Supporting Files/AppSecret.swift create mode 100644 NotificationService/Extension/String.swift create mode 100644 NotificationService/Info.plist create mode 100644 NotificationService/NotificationService+Decrypt.swift create mode 100644 NotificationService/NotificationService.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 0f8236e62..30202e558 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -16,7 +16,7 @@ 0F202227261411BB000C64BF /* HashtagTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */; }; 0F20222D261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */; }; 0F20223326145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223226145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift */; }; - 0F20223926146553000C64BF /* Array+removeDuplicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array+removeDuplicates.swift */; }; + 0F20223926146553000C64BF /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; 0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */; }; 0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */; }; 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FAA101B25E10E760017CCDE /* UIFont.swift */; }; @@ -160,7 +160,9 @@ 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; + 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; + DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; @@ -173,6 +175,7 @@ DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; }; DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; }; DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; }; + DB1E05E1263180F500201847 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E05E0263180F500201847 /* AppSecret.swift */; }; DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */; }; DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E347725F519300079D7DF /* PickServerItem.swift */; }; DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */; }; @@ -220,6 +223,7 @@ DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */; }; DB482A45261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */; }; DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */; }; + DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; }; DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; }; DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; }; DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; }; @@ -246,6 +250,10 @@ DB6B35182601FA3400DC1E11 /* MastodonAttachmentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */; }; DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */; }; DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; + DB6D9F232635195E008423CD /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F222635195E008423CD /* String.swift */; }; + DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; + DB6D9F3B26352019008423CD /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1E05E0263180F500201847 /* AppSecret.swift */; }; + DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */; }; DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */; }; @@ -367,6 +375,10 @@ DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */; }; DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; }; DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; + DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; }; + DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -405,6 +417,13 @@ remoteGlobalIDString = DB89B9ED25C10FD0008580ED; remoteInfo = CoreDataStack; }; + DBF8AE18263293E400C9C23C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DB427DCA25BAA00100D1B89D /* Project object */; + proxyType = 1; + remoteGlobalIDString = DBF8AE12263293E400C9C23C; + remoteInfo = NotificationService; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -420,6 +439,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + DBF8AE1B263293E400C9C23C /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -432,7 +462,7 @@ 0F202226261411BA000C64BF /* HashtagTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; 0F20222C261457EE000C64BF /* HashtagTimelineViewModel+LoadOldestState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadOldestState.swift"; sourceTree = ""; }; 0F20223226145E51000C64BF /* HashtagTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; - 0F20223826146553000C64BF /* Array+removeDuplicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+removeDuplicates.swift"; sourceTree = ""; }; + 0F20223826146553000C64BF /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 0FAA0FDE25E0B57E0017CCDE /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; 0FAA101125E105390017CCDE /* PrimaryActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryActionButton.swift; sourceTree = ""; }; 0FAA101B25E10E760017CCDE /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = ""; }; @@ -574,8 +604,11 @@ 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = ""; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; + 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; }; A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = ""; }; BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = ""; }; @@ -589,6 +622,7 @@ DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = ""; }; DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = ""; }; + DB1E05E0263180F500201847 /* AppSecret.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSecret.swift; sourceTree = ""; }; DB1E346725F518E20079D7DF /* CategoryPickerSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CategoryPickerSection.swift; sourceTree = ""; }; DB1E347725F519300079D7DF /* PickServerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PickServerItem.swift; sourceTree = ""; }; DB1FD43525F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonPickServerViewModel+LoadIndexedServerState.swift"; sourceTree = ""; }; @@ -643,6 +677,7 @@ DB482A3E261331E8008AE74C /* UserTimelineViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewModel+State.swift"; sourceTree = ""; }; DB482A44261335BA008AE74C /* UserTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; DB482A4A261340A7008AE74C /* APIService+UserTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+UserTimeline.swift"; sourceTree = ""; }; + DB4924E126312AB200E9DB22 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = ""; }; DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = ""; }; @@ -668,6 +703,8 @@ DB6B35172601FA3400DC1E11 /* MastodonAttachmentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAttachmentService.swift; sourceTree = ""; }; DB6B351D2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentCollectionViewCell.swift; sourceTree = ""; }; DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; + DB6D9F222635195E008423CD /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; + DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = ""; }; DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarStackContainerButton.swift; sourceTree = ""; }; DB71FD3525F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistMemo.swift"; sourceTree = ""; }; DB71FD3B25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Persist+PersistCache.swift"; sourceTree = ""; }; @@ -790,6 +827,10 @@ DBE3CE12261D7D4200430CC6 /* StatusTableViewControllerAspect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewControllerAspect.swift; sourceTree = ""; }; DBF53F5F25C14E88008AAC7B /* Mastodon.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; name = Mastodon.xctestplan; path = Mastodon/Mastodon.xctestplan; sourceTree = ""; }; DBF53F6025C14E9D008AAC7B /* MastodonSDK.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = MastodonSDK.xctestplan; sourceTree = ""; }; + DBF8AE13263293E400C9C23C /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + DBF8AE15263293E400C9C23C /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + DBF8AE17263293E400C9C23C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -806,6 +847,7 @@ DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, + DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */, @@ -846,6 +888,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DBF8AE10263293E400C9C23C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */, + DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */, + DBF8AE862632992800C9C23C /* Base85 in Frameworks */, + 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -924,6 +977,8 @@ BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */, EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */, 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */, + 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */, + B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1053,6 +1108,7 @@ 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */, DB71FD4B25F8C80E00512AE1 /* StatusPrefetchingService.swift */, DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */, + DB4924E126312AB200E9DB22 /* NotificationService.swift */, ); path = Service; sourceTree = ""; @@ -1216,9 +1272,11 @@ 3FE14AD363ED19AE7FF210A6 /* Frameworks */ = { isa = PBXGroup; children = ( + DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */, A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */, 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */, 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */, + 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */, ); name = Frameworks; sourceTree = ""; @@ -1318,6 +1376,7 @@ children = ( DB427DD525BAA00100D1B89D /* AppDelegate.swift */, DB427DD725BAA00100D1B89D /* SceneDelegate.swift */, + DB1E05E0263180F500201847 /* AppSecret.swift */, DB427DDB25BAA00100D1B89D /* Main.storyboard */, DB427DE025BAA00100D1B89D /* LaunchScreen.storyboard */, DB68A05C25E9055900CFDF14 /* Settings.bundle */, @@ -1347,6 +1406,7 @@ DB427DF625BAA00100D1B89D /* MastodonUITests */, DB89B9EF25C10FD0008580ED /* CoreDataStack */, DB89B9FC25C10FD0008580ED /* CoreDataStackTests */, + DBF8AE14263293E400C9C23C /* NotificationService */, DB427DD325BAA00100D1B89D /* Products */, 1EBA4F56E920856A3FC84ACB /* Pods */, 3FE14AD363ED19AE7FF210A6 /* Frameworks */, @@ -1362,6 +1422,7 @@ DB427DF325BAA00100D1B89D /* MastodonUITests.xctest */, DB89B9EE25C10FD0008580ED /* CoreDataStack.framework */, DB89B9F625C10FD0008580ED /* CoreDataStackTests.xctest */, + DBF8AE13263293E400C9C23C /* NotificationService.appex */, ); name = Products; sourceTree = ""; @@ -1516,6 +1577,14 @@ path = MastodonSDK; sourceTree = ""; }; + DB6D9F2926351961008423CD /* Extension */ = { + isa = PBXGroup; + children = ( + DB6D9F222635195E008423CD /* String.swift */, + ); + path = Extension; + sourceTree = ""; + }; DB72602125E36A2500235243 /* ServerRules */ = { isa = PBXGroup; children = ( @@ -1715,7 +1784,7 @@ 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, 2D84350425FF858100EECE90 /* UIScrollView.swift */, DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */, - 0F20223826146553000C64BF /* Array+removeDuplicates.swift */, + 0F20223826146553000C64BF /* Array.swift */, DBCC3B2F261440A50045B23D /* UITabBarController.swift */, DBCC3B35261440BA0045B23D /* UINavigationController.swift */, ); @@ -1951,6 +2020,17 @@ path = Favorite; sourceTree = ""; }; + DBF8AE14263293E400C9C23C /* NotificationService */ = { + isa = PBXGroup; + children = ( + DBF8AE15263293E400C9C23C /* NotificationService.swift */, + DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */, + DB6D9F2926351961008423CD /* Extension */, + DBF8AE17263293E400C9C23C /* Info.plist */, + ); + path = NotificationService; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1976,11 +2056,13 @@ DB427DD025BAA00100D1B89D /* Resources */, 5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */, DB89BA0825C10FD0008580ED /* Embed Frameworks */, + DBF8AE1B263293E400C9C23C /* Embed App Extensions */, ); buildRules = ( ); dependencies = ( DB89BA0225C10FD0008580ED /* PBXTargetDependency */, + DBF8AE19263293E400C9C23C /* PBXTargetDependency */, ); name = Mastodon; packageProductDependencies = ( @@ -2076,6 +2158,29 @@ productReference = DB89B9F625C10FD0008580ED /* CoreDataStackTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + DBF8AE12263293E400C9C23C /* NotificationService */ = { + isa = PBXNativeTarget; + buildConfigurationList = DBF8AE1E263293E400C9C23C /* Build configuration list for PBXNativeTarget "NotificationService" */; + buildPhases = ( + 0DC740704503CA6BED56F5C8 /* [CP] Check Pods Manifest.lock */, + DBF8AE0F263293E400C9C23C /* Sources */, + DBF8AE10263293E400C9C23C /* Frameworks */, + DBF8AE11263293E400C9C23C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = NotificationService; + packageProductDependencies = ( + DBF8AE852632992800C9C23C /* Base85 */, + DB00CA962632DDB600A54956 /* CommonOSLog */, + DB6D9F41263527CE008423CD /* AlamofireImage */, + ); + productName = NotificationService; + productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -2105,6 +2210,9 @@ CreatedOnToolsVersion = 12.4; TestTargetID = DB427DD125BAA00100D1B89D; }; + DBF8AE12263293E400C9C23C = { + CreatedOnToolsVersion = 12.4; + }; }; }; buildConfigurationList = DB427DCD25BAA00100D1B89D /* Build configuration list for PBXProject "Mastodon" */; @@ -2127,6 +2235,7 @@ DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, + DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -2137,6 +2246,7 @@ DB427DF225BAA00100D1B89D /* MastodonUITests */, DB89B9ED25C10FD0008580ED /* CoreDataStack */, DB89B9F525C10FD0008580ED /* CoreDataStackTests */, + DBF8AE12263293E400C9C23C /* NotificationService */, ); }; /* End PBXProject section */ @@ -2184,9 +2294,38 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DBF8AE11263293E400C9C23C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0DC740704503CA6BED56F5C8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Mastodon-NotificationService-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 5532CB85BBE168B25B20720B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -2551,7 +2690,7 @@ DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */, 5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */, - 0F20223926146553000C64BF /* Array+removeDuplicates.swift in Sources */, + 0F20223926146553000C64BF /* Array.swift in Sources */, 5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, 5DF1058525F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift in Sources */, @@ -2594,6 +2733,7 @@ DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */, 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */, DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */, + DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */, DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */, DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, 0FB3D30825E524C600AAD544 /* PickServerCategoriesCell.swift in Sources */, @@ -2609,6 +2749,7 @@ 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */, DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */, DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */, + DB1E05E1263180F500201847 /* AppSecret.swift in Sources */, DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */, DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */, @@ -2675,6 +2816,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DBF8AE0F263293E400C9C23C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DB6D9F232635195E008423CD /* String.swift in Sources */, + DB6D9F3B26352019008423CD /* AppSecret.swift in Sources */, + DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */, + DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -2703,6 +2855,11 @@ target = DB89B9ED25C10FD0008580ED /* CoreDataStack */; targetProxy = DB89BA0125C10FD0008580ED /* PBXContainerItemProxy */; }; + DBF8AE19263293E400C9C23C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DBF8AE12263293E400C9C23C /* NotificationService */; + targetProxy = DBF8AE18263293E400C9C23C /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -2861,6 +3018,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -2888,6 +3046,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; @@ -3089,6 +3248,48 @@ }; name = Release; }; + DBF8AE1C263293E400C9C23C /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7LFDZ96332; + INFOPLIST_FILE = NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DBF8AE1D263293E400C9C23C /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 7LFDZ96332; + INFOPLIST_FILE = NotificationService/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -3146,6 +3347,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DBF8AE1E263293E400C9C23C /* Build configuration list for PBXNativeTarget "NotificationService" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DBF8AE1C263293E400C9C23C /* Debug */, + DBF8AE1D263293E400C9C23C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -3229,6 +3439,14 @@ kind = branch; }; }; + DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/MainasuK/Base85.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3256,6 +3474,11 @@ isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; + DB00CA962632DDB600A54956 /* CommonOSLog */ = { + isa = XCSwiftPackageProductDependency; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + productName = CommonOSLog; + }; DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; @@ -3271,6 +3494,11 @@ package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; + DB6D9F41263527CE008423CD /* AlamofireImage */ = { + isa = XCSwiftPackageProductDependency; + package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + productName = AlamofireImage; + }; DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { isa = XCSwiftPackageProductDependency; package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; @@ -3286,6 +3514,11 @@ package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; productName = TwitterTextEditor; }; + DBF8AE852632992800C9C23C /* Base85 */ = { + isa = XCSwiftPackageProductDependency; + package = DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */; + productName = Base85; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 18c8840d8..41711ac91 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,22 +7,27 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 13 + 14 Mastodon - RTL.xcscheme_^#shared#^_ orderHint - 9 + 2 Mastodon - Release.xcscheme_^#shared#^_ orderHint - 2 + 0 Mastodon.xcscheme_^#shared#^_ orderHint - 7 + 1 + + NotificationService.xcscheme_^#shared#^_ + + orderHint + 17 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 741947371..a8c0df347 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -37,6 +37,15 @@ "version": "3.1.0" } }, + { + "package": "Base85", + "repositoryURL": "https://github.com/MainasuK/Base85.git", + "state": { + "branch": null, + "revision": "626be96816618689627f806b5c875b5adb6346ef", + "version": "1.0.1" + } + }, { "package": "CommonOSLog", "repositoryURL": "https://github.com/MainasuK/CommonOSLog", diff --git a/Mastodon/Extension/Array+removeDuplicates.swift b/Mastodon/Extension/Array+removeDuplicates.swift deleted file mode 100644 index c3a4b0384..000000000 --- a/Mastodon/Extension/Array+removeDuplicates.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Array+removeDuplicates.swift -// Mastodon -// -// Created by BradGao on 2021/3/31. -// - -import Foundation - -/// https://www.hackingwithswift.com/example-code/language/how-to-remove-duplicate-items-from-an-array -extension Array where Element: Hashable { - func removingDuplicates() -> [Element] { - var addedDict = [Element: Bool]() - - return filter { - addedDict.updateValue(true, forKey: $0) == nil - } - } - - mutating func removeDuplicates() { - self = self.removingDuplicates() - } -} diff --git a/Mastodon/Extension/Array.swift b/Mastodon/Extension/Array.swift new file mode 100644 index 000000000..42f8594d1 --- /dev/null +++ b/Mastodon/Extension/Array.swift @@ -0,0 +1,99 @@ +// +// Array.swift +// Mastodon +// +// Created by BradGao on 2021/3/31. +// + +import Foundation + +/// https://www.hackingwithswift.com/example-code/language/how-to-remove-duplicate-items-from-an-array +extension Array where Element: Hashable { + func removingDuplicates() -> [Element] { + var addedDict = [Element: Bool]() + + return filter { + addedDict.updateValue(true, forKey: $0) == nil + } + } + + mutating func removeDuplicates() { + self = self.removingDuplicates() + } +} + +// +// CryptoSwift +// +// Copyright (C) 2014-2017 Marcin Krzyżanowski +// This software is provided 'as-is', without any express or implied warranty. +// +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: +// +// - The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required. +// - Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. +// - This notice may not be removed or altered from any source or binary distribution. +// + +extension Array { + init(reserveCapacity: Int) { + self = Array() + self.reserveCapacity(reserveCapacity) + } + + var slice: ArraySlice { + self[self.startIndex ..< self.endIndex] + } +} + +extension Array where Element == UInt8 { + public init(hex: String) { + self.init(reserveCapacity: hex.unicodeScalars.lazy.underestimatedCount) + var buffer: UInt8? + var skip = hex.hasPrefix("0x") ? 2 : 0 + for char in hex.unicodeScalars.lazy { + guard skip == 0 else { + skip -= 1 + continue + } + guard char.value >= 48 && char.value <= 102 else { + removeAll() + return + } + let v: UInt8 + let c: UInt8 = UInt8(char.value) + switch c { + case let c where c <= 57: + v = c - 48 + case let c where c >= 65 && c <= 70: + v = c - 55 + case let c where c >= 97: + v = c - 87 + default: + removeAll() + return + } + if let b = buffer { + append(b << 4 | v) + buffer = nil + } else { + buffer = v + } + } + if let b = buffer { + append(b) + } + } + + public func toHexString() -> String { + `lazy`.reduce(into: "") { + var s = String($1, radix: 16) + if s.count == 1 { + s = "0" + s + } + $0 += s + } + } +} diff --git a/Mastodon/Mastodon.entitlements b/Mastodon/Mastodon.entitlements index d334a5e6d..8917adbf4 100644 --- a/Mastodon/Mastodon.entitlements +++ b/Mastodon/Mastodon.entitlements @@ -2,6 +2,8 @@ + aps-environment + development com.apple.security.application-groups group.org.joinmastodon.mastodon-temp diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 4615f92ab..b9ded67d0 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -55,7 +55,7 @@ class SettingsViewController: UIViewController, NeedsDependency { let view = UIStackView() view.translatesAutoresizingMaskIntoConstraints = false view.isLayoutMarginsRelativeArrangement = true - view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4) + //view.layoutMargins = UIEdgeInsets(top: 15, left: 4, bottom: 5, right: 4) view.axis = .horizontal view.alignment = .fill view.distribution = .equalSpacing @@ -270,8 +270,9 @@ extension SettingsViewController: UITableViewDelegate { guard section < sections.count else { return nil } let sectionData = sections[section] + let header: SettingsSectionHeader if section == 1 { - let header = SettingsSectionHeader( + header = SettingsSectionHeader( frame: CGRect(x: 0, y: 0, width: 375, height: 66), customView: notifySectionHeader) header.update(title: sectionData.title) @@ -282,16 +283,19 @@ extension SettingsViewController: UITableViewDelegate { let anyone = L10n.Scene.Settings.Section.Notifications.Trigger.anyone whoButton.setTitle(anyone, for: .normal) } - return header } else { - let header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66)) + header = SettingsSectionHeader(frame: CGRect(x: 0, y: 0, width: 375, height: 66)) header.update(title: sectionData.title) - return header } + + header.preservesSuperviewLayoutMargins = true + + return header } // remove the gap of table's footer func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() } diff --git a/Mastodon/Scene/Settings/SettingsViewModel.swift b/Mastodon/Scene/Settings/SettingsViewModel.swift index 470617aeb..f7ee4c71b 100644 --- a/Mastodon/Scene/Settings/SettingsViewModel.swift +++ b/Mastodon/Scene/Settings/SettingsViewModel.swift @@ -96,49 +96,49 @@ class SettingsViewModel: NSObject, NeedsDependency { func transform(input: Input?) -> Output? { typealias SubscriptionResponse = Mastodon.Response.Content - createSubscriptionSubject - .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) - .sink { _ in - } receiveValue: { [weak self] (arg) in - let (triggerBy, values) = arg - guard let self = self else { - return - } - guard let activeMastodonAuthenticationBox = - self.context.authenticationService.activeMastodonAuthenticationBox.value else { - return - } - guard values.count >= 4 else { - return - } - - self.createDisposeBag.removeAll() - typealias Query = Mastodon.API.Subscriptions.CreateSubscriptionQuery - let domain = activeMastodonAuthenticationBox.domain - let query = Query( - // FIXME: to replace the correct endpoint, p256dh, auth - endpoint: "http://www.google.com", - p256dh: "BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=", - auth: "4vQK-SvRAN5eo-8ASlrwA==", - favourite: values[0], - follow: values[1], - reblog: values[2], - mention: values[3], - poll: nil - ) - self.context.apiService.changeSubscription( - domain: domain, - mastodonAuthenticationBox: activeMastodonAuthenticationBox, - query: query, - triggerBy: triggerBy, - userID: activeMastodonAuthenticationBox.userID - ) - .sink { (_) in - } receiveValue: { (_) in - } - .store(in: &self.createDisposeBag) - } - .store(in: &disposeBag) +// createSubscriptionSubject +// .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) +// .sink { _ in +// } receiveValue: { [weak self] (arg) in +// let (triggerBy, values) = arg +// guard let self = self else { +// return +// } +// guard let activeMastodonAuthenticationBox = +// self.context.authenticationService.activeMastodonAuthenticationBox.value else { +// return +// } +// guard values.count >= 4 else { +// return +// } +// +// self.createDisposeBag.removeAll() +// typealias Query = Mastodon.API.Subscriptions.CreateSubscriptionQuery +// let domain = activeMastodonAuthenticationBox.domain +// let query = Query( +// // FIXME: to replace the correct endpoint, p256dh, auth +// endpoint: "http://www.google.com", +// p256dh: "BLQELIDm-6b9Bl07YrEuXJ4BL_YBVQ0dvt9NQGGJxIQidJWHPNa9YrouvcQ9d7_MqzvGS9Alz60SZNCG3qfpk=", +// auth: "4vQK-SvRAN5eo-8ASlrwA==", +// favourite: values[0], +// follow: values[1], +// reblog: values[2], +// mention: values[3], +// poll: nil +// ) +// self.context.apiService.changeSubscription( +// domain: domain, +// mastodonAuthenticationBox: activeMastodonAuthenticationBox, +// query: query, +// triggerBy: triggerBy, +// userID: activeMastodonAuthenticationBox.userID +// ) +// .sink { (_) in +// } receiveValue: { (_) in +// } +// .store(in: &self.createDisposeBag) +// } +// .store(in: &disposeBag) updateSubscriptionSubject .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) @@ -160,11 +160,16 @@ class SettingsViewModel: NSObject, NeedsDependency { typealias Query = Mastodon.API.Subscriptions.UpdateSubscriptionQuery let domain = activeMastodonAuthenticationBox.domain let query = Query( - favourite: values[0], - follow: values[1], - reblog: values[2], - mention: values[3], - poll: nil) + data: Mastodon.API.Subscriptions.QueryData( + alerts: Mastodon.API.Subscriptions.QueryData.Alerts( + favourite: values[0], + follow: values[1], + reblog: values[2], + mention: values[3], + poll: nil + ) + ) + ) self.context.apiService.updateSubscription( domain: domain, mastodonAuthenticationBox: activeMastodonAuthenticationBox, diff --git a/Mastodon/Service/APIService/APIService+Subscriptions.swift b/Mastodon/Service/APIService/APIService+Subscriptions.swift index 337ab26d2..5260452ce 100644 --- a/Mastodon/Service/APIService/APIService+Subscriptions.swift +++ b/Mastodon/Service/APIService/APIService+Subscriptions.swift @@ -14,11 +14,11 @@ import MastodonSDK extension APIService { func subscription( - domain: String, - userID: String, mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization + let domain = mastodonAuthenticationBox.domain + let userID = mastodonAuthenticationBox.userID let findSettings: Setting? = { let request = Setting.sortedFetchRequest @@ -58,18 +58,21 @@ extension APIService { }.eraseToAnyPublisher() } - func changeSubscription( - domain: String, + func createSubscription( mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox, query: Mastodon.API.Subscriptions.CreateSubscriptionQuery, triggerBy: String, userID: String ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization + let domain = mastodonAuthenticationBox.domain + let userID = mastodonAuthenticationBox.userID - let setting = self.createSettingIfNeed(domain: domain, - userId: userID, - triggerBy: triggerBy) + let setting = self.createSettingIfNeed( + domain: domain, + userId: userID, + triggerBy: triggerBy + ) return Mastodon.API.Subscriptions.createSubscription( session: session, domain: domain, diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift new file mode 100644 index 000000000..098fdff62 --- /dev/null +++ b/Mastodon/Service/NotificationService.swift @@ -0,0 +1,241 @@ +// +// NotificationService.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-4-22. +// + +import os.log +import UIKit +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +final class NotificationService { + + var disposeBag = Set() + + let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.NotificationService.working-queue") + + // input + weak var apiService: APIService? + weak var authenticationService: AuthenticationService? + let isNotificationPermissionGranted = CurrentValueSubject(false) + let deviceToken = CurrentValueSubject(nil) + let mastodonAuthenticationBoxes = CurrentValueSubject<[AuthenticationService.MastodonAuthenticationBox], Never>([]) + + // output + /// [Token: UserID] + let notificationSubscriptionDict: [String: NotificationSubscription] = [:] + + init( + apiService: APIService, + authenticationService: AuthenticationService + ) { + self.apiService = apiService + self.authenticationService = authenticationService + + authenticationService.mastodonAuthentications + .handleEvents(receiveOutput: { [weak self] mastodonAuthentications in + guard let self = self else { return } + + // request permission when sign-in + guard !mastodonAuthentications.isEmpty else { return } + self.requestNotificationPermission() + }) + .map { authentications -> [AuthenticationService.MastodonAuthenticationBox] in + return authentications.compactMap { authentication -> AuthenticationService.MastodonAuthenticationBox? in + return AuthenticationService.MastodonAuthenticationBox( + domain: authentication.domain, + userID: authentication.userID, + appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), + userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken) + ) + } + } + .assign(to: \.value, on: mastodonAuthenticationBoxes) + .store(in: &disposeBag) + + deviceToken + .receive(on: DispatchQueue.main) + .sink { [weak self] deviceToken in + guard let self = self else { return } + guard let deviceToken = deviceToken else { return } + let token = [UInt8](deviceToken).toHexString() + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: deviceToken: %s", ((#file as NSString).lastPathComponent), #line, #function, token) + } + .store(in: &disposeBag) + + Publishers.CombineLatest3( + isNotificationPermissionGranted, + deviceToken, + mastodonAuthenticationBoxes + ) + .sink { [weak self] isNotificationPermissionGranted, deviceToken, mastodonAuthenticationBoxes in + guard let self = self else { return } + guard isNotificationPermissionGranted else { return } + guard let deviceToken = deviceToken else { return } + self.registerNotificationSubscriptions( + deviceToken: [UInt8](deviceToken).toHexString(), + mastodonAuthenticationBoxes: mastodonAuthenticationBoxes + ) + } + .store(in: &disposeBag) + } + +} + +extension NotificationService { + private func requestNotificationPermission() { + let center = UNUserNotificationCenter.current() + center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, error in + guard let self = self else { return } + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: request notification permission: %s", ((#file as NSString).lastPathComponent), #line, #function, granted ? "granted" : "fail") + + self.isNotificationPermissionGranted.value = granted + + if let _ = error { + // Handle the error here. + } + + // Enable or disable features based on the authorization. + } + } + + private func registerNotificationSubscriptions( + deviceToken: String, + mastodonAuthenticationBoxes: [AuthenticationService.MastodonAuthenticationBox] + ) { + for mastodonAuthenticationBox in mastodonAuthenticationBoxes { + guard let notificationSubscription = dequeueNotificationSubscription(mastodonAuthenticationBox: mastodonAuthenticationBox) else { continue } + let token = NotificationSubscription.SubscribeToken( + deviceToken: deviceToken, + authenticationBox: mastodonAuthenticationBox + ) + guard let subscription = subscribe( + notificationSubscription: notificationSubscription, + token: token + ) else { continue } + + subscription + .sink { completion in + // handle error + } receiveValue: { response in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: did create subscription %s with userToken %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, mastodonAuthenticationBox.userAuthorization.accessToken) + // do nothing + } + .store(in: &self.disposeBag) + } + } + + private func dequeueNotificationSubscription(mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> NotificationSubscription? { + var _notificationSubscription: NotificationSubscription? + workingQueue.sync { + let domain = mastodonAuthenticationBox.domain + let userID = mastodonAuthenticationBox.userID + let key = [domain, userID].joined(separator: "@") + + if let notificationSubscription = notificationSubscriptionDict[key] { + _notificationSubscription = notificationSubscription + } else { + let notificationSubscription = NotificationSubscription(domain: domain, userID: userID) + _notificationSubscription = notificationSubscription + } + } + return _notificationSubscription + } + + private func subscribe( + notificationSubscription: NotificationSubscription, + token: NotificationSubscription.SubscribeToken + ) -> AnyPublisher, Error>? { + guard let apiService = self.apiService else { return nil } + + if let oldToken = notificationSubscription.token { + guard oldToken != token else { return nil } + } + notificationSubscription.token = token + + let appSecret = AppSecret.default + let endpoint = appSecret.notificationEndpoint + "/" + token.deviceToken + let p256dh = appSecret.uncompressionNotificationPublicKeyData + let auth = appSecret.notificationAuth + + let query = Mastodon.API.Subscriptions.CreateSubscriptionQuery( + subscription: Mastodon.API.Subscriptions.QuerySubscription( + endpoint: endpoint, + keys: Mastodon.API.Subscriptions.QuerySubscription.Keys( + p256dh: p256dh, + auth: auth + ) + ), + data: Mastodon.API.Subscriptions.QueryData( + alerts: Mastodon.API.Subscriptions.QueryData.Alerts( + favourite: true, + follow: true, + reblog: true, + mention: true, + poll: true + ) + ) + ) + + return apiService.createSubscription( + mastodonAuthenticationBox: token.authenticationBox, + query: query, + triggerBy: "anyone", + userID: token.authenticationBox.userID + ) + } + + static func createRandomAuthBytes() -> Data { + let byteCount = 16 + var bytes = Data(count: byteCount) + _ = bytes.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, byteCount, $0.baseAddress!) } + return bytes + } +} + +extension NotificationService { + final class NotificationSubscription { + + var disposeBag = Set() + + // input + let domain: String + let userID: Mastodon.Entity.Account.ID + + var token: SubscribeToken? + + init(domain: String, userID: Mastodon.Entity.Account.ID) { + self.domain = domain + self.userID = userID + } + + struct SubscribeToken: Equatable { + + let deviceToken: String + let authenticationBox: AuthenticationService.MastodonAuthenticationBox + // TODO: set other parameter + + init( + deviceToken: String, + authenticationBox: AuthenticationService.MastodonAuthenticationBox + ) { + self.deviceToken = deviceToken + self.authenticationBox = authenticationBox + } + + static func == ( + lhs: NotificationService.NotificationSubscription.SubscribeToken, + rhs: NotificationService.NotificationSubscription.SubscribeToken + ) -> Bool { + return lhs.deviceToken == rhs.deviceToken && + lhs.authenticationBox.domain == rhs.authenticationBox.domain && + lhs.authenticationBox.userID == rhs.authenticationBox.userID && + lhs.authenticationBox.userAuthorization.accessToken == rhs.authenticationBox.userAuthorization.accessToken + } + } + } +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 903cb7693..1ed2c283f 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -28,6 +28,7 @@ class AppContext: ObservableObject { let videoPlaybackService = VideoPlaybackService() let statusPrefetchingService: StatusPrefetchingService let statusPublishService = StatusPublishService() + let notificationService: NotificationService let documentStore: DocumentStore private var documentStoreSubscription: AnyCancellable! @@ -45,11 +46,12 @@ class AppContext: ObservableObject { let _apiService = APIService(backgroundManagedObjectContext: _backgroundManagedObjectContext) apiService = _apiService - authenticationService = AuthenticationService( + let _authenticationService = AuthenticationService( managedObjectContext: _managedObjectContext, backgroundManagedObjectContext: _backgroundManagedObjectContext, apiService: _apiService ) + authenticationService = _authenticationService emojiService = EmojiService( apiService: apiService @@ -57,6 +59,10 @@ class AppContext: ObservableObject { statusPrefetchingService = StatusPrefetchingService( apiService: _apiService ) + notificationService = NotificationService( + apiService: _apiService, + authenticationService: _authenticationService + ) documentStore = DocumentStore() documentStoreSubscription = documentStore.objectWillChange diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 72ee1334d..fd4c23004 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -5,6 +5,7 @@ // Created by MainasuK Cirno on 2021/1/22. // +import os.log import UIKit @main @@ -18,6 +19,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UserDefaults.standard.setValue(UIApplication.appVersion(), forKey: "Mastodon.appVersion") UserDefaults.standard.setValue(UIApplication.appBuild(), forKey: "Mastodon.appBundle") +// UNUserNotificationCenter.current().delegate = self + application.registerForRemoteNotifications() + return true } @@ -38,13 +42,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } - extension AppDelegate { func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return UIDevice.current.userInterfaceIdiom == .phone ? .portrait : .all } } +extension AppDelegate { + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + appContext.notificationService.deviceToken.value = deviceToken + } +} + +// MARK: - UNUserNotificationCenterDelegate +extension AppDelegate: UNUserNotificationCenterDelegate { + +} extension AppContext { static var shared: AppContext { diff --git a/Mastodon/Supporting Files/AppSecret.swift b/Mastodon/Supporting Files/AppSecret.swift new file mode 100644 index 000000000..0a30553a7 --- /dev/null +++ b/Mastodon/Supporting Files/AppSecret.swift @@ -0,0 +1,51 @@ +// +// AppSecret.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-4-22. +// + +import Foundation +import CryptoKit +import Keys + +final class AppSecret { + + let notificationEndpoint: String + + let notificationPrivateKey: P256.KeyAgreement.PrivateKey! + let notificationPublicKey: P256.KeyAgreement.PublicKey! + let notificationAuth: Data + + static let `default`: AppSecret = { + return AppSecret() + }() + + init() { + let keys = MastodonKeys() + + #if DEBUG + self.notificationEndpoint = keys.notification_endpoint_debug + let nonce = keys.notification_key_nonce_debug + let auth = keys.notification_key_auth_debug + #else + self.notificationEndpoint = keys.notification_endpoint + let nonce = keys.notification_key_nonce + let auth = keys.notification_key_auth + #endif + + notificationPrivateKey = try! P256.KeyAgreement.PrivateKey(rawRepresentation: Data(base64Encoded: nonce)!) + notificationPublicKey = notificationPrivateKey!.publicKey + notificationAuth = Data(base64Encoded: auth)! + } + + var uncompressionNotificationPublicKeyData: Data { + var data = notificationPublicKey.rawRepresentation + if data.count == 64 { + let prefix: [UInt8] = [0x04] + data = Data(prefix) + data + } + return data + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift index df9168499..f78c2c6c6 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Push.swift @@ -117,110 +117,75 @@ extension Mastodon.API.Subscriptions { } extension Mastodon.API.Subscriptions { - public struct CreateSubscriptionQuery: Codable, PostQuery { + + public struct QuerySubscription: Codable { let endpoint: String - let p256dh: String - let auth: String - let favourite: Bool? - let follow: Bool? - let reblog: Bool? - let mention: Bool? - let poll: Bool? - - var queryItems: [URLQueryItem]? { - var items = [URLQueryItem]() - - items.append(URLQueryItem(name: "subscription[endpoint]", value: endpoint)) - items.append(URLQueryItem(name: "subscription[keys][p256dh]", value: p256dh)) - items.append(URLQueryItem(name: "subscription[keys][auth]", value: auth)) - - if let followValue = follow?.queryItemValue { - let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue) - items.append(followItem) - } - - if let favouriteValue = favourite?.queryItemValue { - let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue) - items.append(favouriteItem) - } - - if let reblogValue = reblog?.queryItemValue { - let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue) - items.append(reblogItem) - } - - if let mentionValue = mention?.queryItemValue { - let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue) - items.append(mentionItem) - } - return items - } + let keys: Keys public init( endpoint: String, - p256dh: String, - auth: String, - favourite: Bool?, - follow: Bool?, - reblog: Bool?, - mention: Bool?, - poll: Bool? + keys: Keys ) { self.endpoint = endpoint - self.p256dh = p256dh - self.auth = auth - self.favourite = favourite - self.follow = follow - self.reblog = reblog - self.mention = mention - self.poll = poll + self.keys = keys + } + + public struct Keys: Codable { + let p256dh: String + let auth: String + + public init(p256dh: Data, auth: Data) { + self.p256dh = p256dh.base64UrlEncodedString() + self.auth = auth.base64UrlEncodedString() + } + } + } + + public struct QueryData: Codable { + let alerts: Alerts + + public init(alerts: Mastodon.API.Subscriptions.QueryData.Alerts) { + self.alerts = alerts + } + + public struct Alerts: Codable { + let favourite: Bool? + let follow: Bool? + let reblog: Bool? + let mention: Bool? + let poll: Bool? + + public init(favourite: Bool?, follow: Bool?, reblog: Bool?, mention: Bool?, poll: Bool?) { + self.favourite = favourite + self.follow = follow + self.reblog = reblog + self.mention = mention + self.poll = poll + } + } + } + + public struct CreateSubscriptionQuery: Codable, PostQuery { + let subscription: QuerySubscription + let data: QueryData + + public init( + subscription: Mastodon.API.Subscriptions.QuerySubscription, + data: Mastodon.API.Subscriptions.QueryData + ) { + self.subscription = subscription + self.data = data } } public struct UpdateSubscriptionQuery: Codable, PutQuery { - let favourite: Bool? - let follow: Bool? - let reblog: Bool? - let mention: Bool? - let poll: Bool? - var queryItems: [URLQueryItem]? { - var items = [URLQueryItem]() - - if let followValue = follow?.queryItemValue { - let followItem = URLQueryItem(name: "data[alerts][follow]", value: followValue) - items.append(followItem) - } - - if let favouriteValue = favourite?.queryItemValue { - let favouriteItem = URLQueryItem(name: "data[alerts][favourite]", value: favouriteValue) - items.append(favouriteItem) - } - - if let reblogValue = reblog?.queryItemValue { - let reblogItem = URLQueryItem(name: "data[alerts][reblog]", value: reblogValue) - items.append(reblogItem) - } - - if let mentionValue = mention?.queryItemValue { - let mentionItem = URLQueryItem(name: "data[alerts][mention]", value: mentionValue) - items.append(mentionItem) - } - return items + let data: QueryData + + public init(data: Mastodon.API.Subscriptions.QueryData) { + self.data = data } - public init( - favourite: Bool?, - follow: Bool?, - reblog: Bool?, - mention: Bool?, - poll: Bool? - ) { - self.favourite = favourite - self.follow = follow - self.reblog = reblog - self.mention = mention - self.poll = poll - } + var queryItems: [URLQueryItem]? { nil } } } diff --git a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift index 43354394d..48e442b9d 100644 --- a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift +++ b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift @@ -35,3 +35,12 @@ extension Data { } } + +extension Data { + func base64UrlEncodedString() -> String { + return base64EncodedString() + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } +} diff --git a/NotificationService/Extension/String.swift b/NotificationService/Extension/String.swift new file mode 100644 index 000000000..edb162428 --- /dev/null +++ b/NotificationService/Extension/String.swift @@ -0,0 +1,31 @@ +// +// String.swift +// NotificationService +// +// Created by MainasuK Cirno on 2021-4-25. +// + +import Foundation + +extension String { + static func normalize(base64String: String) -> String { + let base64 = base64String + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + .padding() + return base64 + } + + private func padding() -> String { + let remainder = self.count % 4 + if remainder > 0 { + return self.padding( + toLength: self.count + 4 - remainder, + withPad: "=", + startingAt: 0 + ) + } + return self + } +} + diff --git a/NotificationService/Info.plist b/NotificationService/Info.plist new file mode 100644 index 000000000..7db9ec9cb --- /dev/null +++ b/NotificationService/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + NotificationService + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/NotificationService/NotificationService+Decrypt.swift b/NotificationService/NotificationService+Decrypt.swift new file mode 100644 index 000000000..065863fda --- /dev/null +++ b/NotificationService/NotificationService+Decrypt.swift @@ -0,0 +1,96 @@ +// +// NotificationService+Decrypt.swift +// NotificationService +// +// Created by MainasuK Cirno on 2021-4-25. +// + +import os.log +import Foundation +import CryptoKit + +extension NotificationService { + + static func decrypt(payload: Data, salt: Data, auth: Data, privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) -> Data? { + guard let sharedSecret = try? privateKey.sharedSecretFromKeyAgreement(with: publicKey) else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to craete shared secret", ((#file as NSString).lastPathComponent), #line, #function) + return nil + } + + let keyMaterial = sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: auth, sharedInfo: Data("Content-Encoding: auth\0".utf8), outputByteCount: 32) + + let keyInfo = info(type: "aesgcm", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation) + let key = HKDF.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: keyInfo, outputByteCount: 16) + + let nonceInfo = info(type: "nonce", clientPublicKey: privateKey.publicKey.x963Representation, serverPublicKey: publicKey.x963Representation) + let nonce = HKDF.deriveKey(inputKeyMaterial: keyMaterial, salt: salt, info: nonceInfo, outputByteCount: 12) + + let nonceData = nonce.withUnsafeBytes(Array.init) + + guard let sealedBox = try? AES.GCM.SealedBox(combined: nonceData + payload) else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to create sealedBox", ((#file as NSString).lastPathComponent), #line, #function) + return nil + } + + guard let plaintext = try? AES.GCM.open(sealedBox, using: key) else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: failed to open sealedBox", ((#file as NSString).lastPathComponent), #line, #function) + return nil + } + + let paddingLength = Int(plaintext[0]) * 256 + Int(plaintext[1]) + guard plaintext.count >= 2 + paddingLength else { + print("1") + fatalError() + } + let unpadded = plaintext.suffix(from: paddingLength + 2) + + return Data(unpadded) + } + + static private func info(type: String, clientPublicKey: Data, serverPublicKey: Data) -> Data { + var info = Data() + + info.append("Content-Encoding: ".data(using: .utf8)!) + info.append(type.data(using: .utf8)!) + info.append(0) + info.append("P-256".data(using: .utf8)!) + info.append(0) + info.append(0) + info.append(65) + info.append(clientPublicKey) + info.append(0) + info.append(65) + info.append(serverPublicKey) + + return info + } +} + +extension NotificationService { + struct MastodonNotification: Codable { + + private let _accessToken: String + var accessToken: String { + return String.normalize(base64String: _accessToken) + } + + let notificationID: Int + let notificationType: String + + let preferredLocale: String? + let icon: String? + let title: String + let body: String + + enum CodingKeys: String, CodingKey { + case _accessToken = "access_token" + case notificationID = "notification_id" + case notificationType = "notification_type" + case preferredLocale = "preferred_locale" + case icon + case title + case body + } + + } +} diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift new file mode 100644 index 000000000..911866f17 --- /dev/null +++ b/NotificationService/NotificationService.swift @@ -0,0 +1,103 @@ +// +// NotificationService.swift +// NotificationService +// +// Created by MainasuK Cirno on 2021-4-23. +// + +import UserNotifications +import CommonOSLog +import CryptoKit +import AlamofireImage +import Base85 +import Keys + +class NotificationService: UNNotificationServiceExtension { + + var contentHandler: ((UNNotificationContent) -> Void)? + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.contentHandler = contentHandler + bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + if let bestAttemptContent = bestAttemptContent { + // Modify the notification content here... + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + let privateKey = AppSecret.default.notificationPrivateKey! + + guard let encodedPayload = bestAttemptContent.userInfo["p"] as? String, + let payload = Data(base85Encoded: encodedPayload, options: [], encoding: .z85) else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid payload", ((#file as NSString).lastPathComponent), #line, #function) + contentHandler(bestAttemptContent) + return + } + + guard let encodedPublicKey = bestAttemptContent.userInfo["k"] as? String, + let publicKey = NotificationService.publicKey(encodedPublicKey: encodedPublicKey) else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid public key", ((#file as NSString).lastPathComponent), #line, #function) + contentHandler(bestAttemptContent) + return + } + + guard let encodedSalt = bestAttemptContent.userInfo["s"] as? String, + let salt = Data(base85Encoded: encodedSalt, options: [], encoding: .z85) else { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: invalid salt", ((#file as NSString).lastPathComponent), #line, #function) + contentHandler(bestAttemptContent) + return + } + + let auth = AppSecret.default.notificationAuth + guard let plaintextData = NotificationService.decrypt(payload: payload, salt: salt, auth: auth, privateKey: privateKey, publicKey: publicKey), + let notification = try? JSONDecoder().decode(MastodonNotification.self, from: plaintextData) else { + contentHandler(bestAttemptContent) + return + } + + bestAttemptContent.title = notification.title + bestAttemptContent.subtitle = "" + bestAttemptContent.body = notification.body + + if let urlString = notification.icon, let url = URL(string: urlString) { + let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("notification-attachments") + try? FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true, attributes: nil) + let filename = url.lastPathComponent + let fileURL = temporaryDirectoryURL.appendingPathComponent(filename) + + ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in + guard let _ = self else { return } + switch response.result { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) + case .success(let image): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) + try? image.pngData()?.write(to: fileURL) + if let attachment = try? UNNotificationAttachment(identifier: filename, url: fileURL, options: nil) { + bestAttemptContent.attachments = [attachment] + } + } + contentHandler(bestAttemptContent) + }) + } else { + contentHandler(bestAttemptContent) + } + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + contentHandler(bestAttemptContent) + } + } + +} + +extension NotificationService { + static func publicKey(encodedPublicKey: String) -> P256.KeyAgreement.PublicKey? { + guard let publicKeyData = Data(base85Encoded: encodedPublicKey, options: [], encoding: .z85) else { return nil } + return try? P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) + } +} diff --git a/Podfile b/Podfile index d4bec65d4..bb929277e 100644 --- a/Podfile +++ b/Podfile @@ -23,4 +23,20 @@ target 'Mastodon' do # Pods for testing end + target 'NotificationService' do + + end + end + +plugin 'cocoapods-keys', { + :project => "Mastodon", + :keys => [ + "notification_endpoint", + "notification_endpoint_debug", + "notification_key_nonce", + "notification_key_nonce_debug", + "notification_key_auth", + "notification_key_auth_debug" + ] +} \ No newline at end of file diff --git a/Podfile.lock b/Podfile.lock index 4f553c4e3..d34a2ada5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,12 +1,14 @@ PODS: - DateToolsSwift (5.0.0) - Kanna (5.2.4) + - Keys (1.0.1) - SwiftGen (6.4.0) - "UITextField+Shake (1.2.1)" DEPENDENCIES: - DateToolsSwift (~> 5.0.0) - Kanna (~> 5.2.2) + - Keys (from `Pods/CocoaPodsKeys`) - SwiftGen (~> 6.4.0) - "UITextField+Shake (~> 1.2)" @@ -17,12 +19,17 @@ SPEC REPOS: - SwiftGen - "UITextField+Shake" +EXTERNAL SOURCES: + Keys: + :path: Pods/CocoaPodsKeys + SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f + Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 -PODFILE CHECKSUM: 30e8e3a555251a512e7b5e91183747152f126e7a +PODFILE CHECKSUM: b99204f58cb11d471cfad7269bbf0abb853dc953 COCOAPODS: 1.10.1 diff --git a/README.md b/README.md index 23957fa16..d3cddb071 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ arch -x86_64 pod install - [AlamofireNetworkActivityIndicator](https://github.com/Alamofire/AlamofireNetworkActivityIndicator) - [Alamofire](https://github.com/Alamofire/Alamofire) - [CommonOSLog](https://github.com/mainasuk/CommonOSLog) +- [CryptoSwift](https://github.com/krzyzanowskim/CryptoSwift) - [DateToolSwift](https://github.com/MatthewYork/DateTools) - [Kanna](https://github.com/tid-kijyun/Kanna) - [Kingfisher](https://github.com/onevcat/Kingfisher)