diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index f3328cbf0..1c9c6c724 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; }; 2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */; }; 2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; }; - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; @@ -20,7 +20,7 @@ 2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D46976325C2A71500CF4AA9 /* UIIamge.swift */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Timeline.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; }; @@ -35,7 +35,7 @@ 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; 3533495136D843E85211E3E2 /* Pods_Mastodon_MastodonUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A1B4523A7981F1044DE89C21 /* Pods_Mastodon_MastodonUITests.framework */; }; 45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; 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 */; }; 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 602D783BEC22881EBAD84419 /* Pods_Mastodon.framework */; }; @@ -43,12 +43,10 @@ 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 */; }; - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; - DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102225BAA7B400EAA174 /* Assets.swift */; }; - DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3D102325BAA7B400EAA174 /* Strings.swift */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; DB427DDD25BAA00100D1B89D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDB25BAA00100D1B89D /* Main.storyboard */; }; @@ -80,6 +78,12 @@ DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; }; DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; }; DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */; }; + DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; }; + DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; }; + DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; + DB98338725C945ED00AD9700 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338525C945ED00AD9700 /* Strings.swift */; }; + DB98338825C945ED00AD9700 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98338625C945ED00AD9700 /* Assets.swift */; }; + DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -176,8 +180,6 @@ DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3D100E25BAA75E00EAA174 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - DB3D102225BAA7B400EAA174 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; - DB3D102325BAA7B400EAA174 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; DB427DD225BAA00100D1B89D /* Mastodon.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Mastodon.app; sourceTree = BUILT_PRODUCTS_DIR; }; DB427DD525BAA00100D1B89D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DB427DD725BAA00100D1B89D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -217,6 +219,12 @@ DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = ""; }; DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = ""; }; DB98334625C8056600AD9700 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; + DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; }; + DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; + DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; + DB98338525C945ED00AD9700 /* Strings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; + DB98338625C945ED00AD9700 /* Assets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Assets.swift; sourceTree = ""; }; + DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.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 = ""; }; 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 = ""; }; @@ -227,13 +235,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, 7A9135D4559750AF07CA9BE4 /* Pods_Mastodon.framework in Frameworks */, - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, 45B49097460EDE530AD5AA72 /* Pods_Mastodon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -315,6 +323,10 @@ isa = PBXGroup; children = ( 2D61335D25C1894B00CAE157 /* APIService.swift */, + DB98337E25C9452D00AD9700 /* APIService+APIError.swift */, + DB98336A25C9420100AD9700 /* APIService+App.swift */, + DB98337025C9443200AD9700 /* APIService+Authentication.swift */, + DB98339B25C96DE600AD9700 /* APIService+Account.swift */, 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */, 2D61335625C1887F00CAE157 /* Persist */, ); @@ -444,15 +456,6 @@ path = Resources; sourceTree = ""; }; - DB3D101B25BAA79200EAA174 /* Generated */ = { - isa = PBXGroup; - children = ( - DB3D102225BAA7B400EAA174 /* Assets.swift */, - DB3D102325BAA7B400EAA174 /* Strings.swift */, - ); - path = Generated; - sourceTree = ""; - }; DB427DC925BAA00100D1B89D = { isa = PBXGroup; children = ( @@ -467,6 +470,7 @@ DB427DD325BAA00100D1B89D /* Products */, 1EBA4F56E920856A3FC84ACB /* Pods */, 3FE14AD363ED19AE7FF210A6 /* Frameworks */, + DB98335F25C93B0400AD9700 /* Recovered References */, ); sourceTree = ""; }; @@ -485,15 +489,15 @@ DB427DD425BAA00100D1B89D /* Mastodon */ = { isa = PBXGroup; children = ( - DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, DB427DE325BAA00100D1B89D /* Info.plist */, + DB89BA1025C10FF5008580ED /* Mastodon.entitlements */, 2D76319C25C151DE00929FB9 /* Diffiable */, DB8AF52A25C13561002E6C99 /* State */, 2D61335525C1886800CAE157 /* Service */, - DB8AF56225C138BC002E6C99 /* Extension */, DB8AF55525C1379F002E6C99 /* Scene */, DB8AF54125C13647002E6C99 /* Coordinator */, - DB3D101B25BAA79200EAA174 /* Generated */, + DB8AF56225C138BC002E6C99 /* Extension */, + DB98338425C945ED00AD9700 /* Generated */, DB3D0FF825BAA6B200EAA174 /* Resources */, DB3D0FF725BAA68500EAA174 /* Supporting Files */, ); @@ -578,9 +582,9 @@ DB8AF52A25C13561002E6C99 /* State */ = { isa = PBXGroup; children = ( + DB8AF52D25C13561002E6C99 /* AppContext.swift */, DB8AF52B25C13561002E6C99 /* ViewStateStore.swift */, DB8AF52C25C13561002E6C99 /* DocumentStore.swift */, - DB8AF52D25C13561002E6C99 /* AppContext.swift */, ); path = State; sourceTree = ""; @@ -606,10 +610,10 @@ isa = PBXGroup; children = ( 2D7631A425C1532200929FB9 /* Share */, + DB01409B25C40BB600F9F3CF /* Authentication */, 2D76316325C14BAC00929FB9 /* PublicTimeline */, DB8AF54E25C13703002E6C99 /* MainTab */, DB8AF55625C137A8002E6C99 /* HomeViewController.swift */, - DB01409B25C40BB600F9F3CF /* Authentication */, ); path = Scene; sourceTree = ""; @@ -628,6 +632,23 @@ path = Extension; sourceTree = ""; }; + DB98335F25C93B0400AD9700 /* Recovered References */ = { + isa = PBXGroup; + children = ( + CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */, + ); + name = "Recovered References"; + sourceTree = ""; + }; + DB98338425C945ED00AD9700 /* Generated */ = { + isa = PBXGroup; + children = ( + DB98338525C945ED00AD9700 /* Strings.swift */, + DB98338625C945ED00AD9700 /* Assets.swift */, + ); + path = Generated; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -661,11 +682,11 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, + DB3D0FF225BAA61700EAA174 /* AlamofireImage */, + 5D526FE125BE9AC400460CB9 /* MastodonSDK */, + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, + 2D42FF6025C8177C004A627A /* ActiveLabel */, + DB0140BC25C40D7500F9F3CF /* CommonOSLog */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -788,10 +809,10 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -973,6 +994,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */, 2D7631B325C159F700929FB9 /* Item.swift in Sources */, 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */, DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */, @@ -980,7 +1002,9 @@ 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */, 2D42FF8F25C8228A004A627A /* UIButton.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Timeline.swift in Sources */, + DB98338825C945ED00AD9700 /* Assets.swift in Sources */, DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */, + DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */, 2D46975E25C2A54100CF4AA9 /* NSLayoutConstraint.swift in Sources */, @@ -995,18 +1019,19 @@ 2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */, DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */, + DB98338725C945ED00AD9700 /* Strings.swift in Sources */, + DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */, 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */, 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */, + DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */, 2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */, DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */, DB8AF55725C137A8002E6C99 /* HomeViewController.swift in Sources */, 2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */, 2D7631A825C1535600929FB9 /* TimelinePostTableViewCell.swift in Sources */, - DB3D102525BAA7B400EAA174 /* Strings.swift in Sources */, 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */, DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */, - DB3D102425BAA7B400EAA174 /* Assets.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, ); @@ -1518,7 +1543,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { @@ -1526,7 +1551,7 @@ minimumVersion = 3.0.0; }; }; - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; requirement = { @@ -1534,7 +1559,7 @@ minimumVersion = 3.1.0; }; }; - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -1542,7 +1567,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -1553,28 +1578,28 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { + 2D42FF6025C8177C004A627A /* ActiveLabel */ = { isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; + package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; productName = ActiveLabel; }; - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; + package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; productName = AlamofireNetworkActivityIndicator; }; - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { + 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { + DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { + DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; + package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index a8d3dbb8a..0c8388901 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -38,6 +38,7 @@ extension SceneCoordinator { enum Scene { case authentication(viewModel: AuthenticationViewModel) + case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel) } } @@ -113,6 +114,10 @@ private extension SceneCoordinator { let _viewController = AuthenticationViewController() _viewController.viewModel = viewModel viewController = _viewController + case .mastodonPinBasedAuthentication(let viewModel): + let _viewController = MastodonPinBasedAuthenticationViewController() + _viewController.viewModel = viewModel + viewController = _viewController } setupDependency(for: viewController as? NeedsDependency) diff --git a/Mastodon/Scene/Authentication/AuthenticationViewController.swift b/Mastodon/Scene/Authentication/AuthenticationViewController.swift index bb66b1e63..dadad28a0 100644 --- a/Mastodon/Scene/Authentication/AuthenticationViewController.swift +++ b/Mastodon/Scene/Authentication/AuthenticationViewController.swift @@ -8,6 +8,7 @@ import os.log import UIKit import Combine +import MastodonSDK final class AuthenticationViewController: UIViewController, NeedsDependency { @@ -17,6 +18,7 @@ final class AuthenticationViewController: UIViewController, NeedsDependency { weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var viewModel: AuthenticationViewModel! + var mastodonPinBasedAuthenticationViewController: UIViewController? let domainTextField: UITextField = { let textField = UITextField() @@ -75,6 +77,18 @@ extension AuthenticationViewController { @objc private func signInBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + guard let domain = viewModel.domain.value else { + // TODO: alert error + return + } + viewModel.signInAction.send(domain) } } + +// MARK: - UIAdaptivePresentationControllerDelegate +extension AuthenticationViewController: UIAdaptivePresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + return .fullScreen + } +} diff --git a/Mastodon/Scene/Authentication/AuthenticationViewModel.swift b/Mastodon/Scene/Authentication/AuthenticationViewModel.swift index 4d0e21f33..264ed487c 100644 --- a/Mastodon/Scene/Authentication/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Authentication/AuthenticationViewModel.swift @@ -5,21 +5,34 @@ // Created by MainasuK Cirno on 2021/2/1. // -import Foundation +import os.log +import UIKit import Combine +import MastodonSDK final class AuthenticationViewModel { var disposeBag = Set() // input + let context: AppContext + let coordinator: SceneCoordinator let input = CurrentValueSubject("") + let signInAction = PassthroughSubject() // output let domain = CurrentValueSubject(nil) let isSignInButtonEnabled = CurrentValueSubject(false) + let isAuthenticating = CurrentValueSubject(false) + let authenticated = PassthroughSubject() + let error = CurrentValueSubject(nil) - init() { + private var mastodonPinBasedAuthenticationViewController: UIViewController? + + init(context: AppContext, coordinator: SceneCoordinator) { + self.context = context + self.coordinator = coordinator + input .map { input in let trimmed = input.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() @@ -39,10 +52,135 @@ final class AuthenticationViewModel { .store(in: &disposeBag) domain - .print() .map { $0 != nil } .assign(to: \.value, on: isSignInButtonEnabled) .store(in: &disposeBag) + + signInAction + .handleEvents(receiveOutput: { [weak self] _ in + // trigger state change + guard let self = self else { return } + self.isAuthenticating.value = true + }) + .flatMap { domain in + context.apiService.createApplication(domain: domain) + .retry(3) + .tryMap { response -> AuthenticateInfo in + let application = response.value + guard let clientID = application.clientID, + let clientSecret = application.clientSecret else { + throw APIService.APIError.explicit(.badResponse) + } + let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID) + let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) + return AuthenticateInfo( + domain: domain, + clientID: clientID, + clientSecret: clientSecret, + url: url + ) + } + } + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + // trigger state update + self.isAuthenticating.value = false + + switch completion { + case .failure(let error): + // TODO: handle error + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + self.error.value = error + case .finished: + break + } + } receiveValue: { [weak self] info in + guard let self = self else { return } + let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.url) + self.authenticate( + info: info, + pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher + ) + self.mastodonPinBasedAuthenticationViewController = self.coordinator.present( + scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel), + from: nil, + transition: .modal(animated: true, completion: nil) + ) + } + .store(in: &disposeBag) + } + +} + +extension AuthenticationViewModel { + + struct AuthenticateInfo { + let domain: String + let clientID: String + let clientSecret: String + let url: URL + } + + func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject) { + pinCodePublisher + .handleEvents(receiveOutput: { [weak self] _ in + guard let self = self else { return } + self.isAuthenticating.value = true + self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil) + self.mastodonPinBasedAuthenticationViewController = nil + }) + .compactMap { [weak self] code -> AnyPublisher, Error>? in + guard let self = self else { return nil } + return self.context.apiService + .userAccessToken( + domain: info.domain, + clientID: info.clientID, + clientSecret: info.clientSecret, + code: code + ) + .flatMap { response -> AnyPublisher, Error> in + let token = response.value + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in success. Token: %s", ((#file as NSString).lastPathComponent), #line, #function, token.accessToken) + return AuthenticationViewModel.verifyAndSaveAuthentication( + context: self.context, + info: info, + token: token + ) + } + .eraseToAnyPublisher() + } + .switchToLatest() + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: swap user access token swap fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + self.isAuthenticating.value = false + self.error.value = error + case .finished: + break + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + let account = response.value + // TODO: + } + .store(in: &self.disposeBag) + } + + static func verifyAndSaveAuthentication( + context: AppContext, + info: AuthenticateInfo, + token: Mastodon.Entity.Token + ) -> AnyPublisher, Error> { + let authorization = Mastodon.API.OAuth.Authorization(accessToken: token.accessToken) + return context.apiService.accountVerifyCredentials( + domain: info.domain, + authorization: authorization + ) + // TODO: add persist logic } } diff --git a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift index 6f9ce506f..fa57ddfd4 100644 --- a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift +++ b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewController.swift @@ -6,20 +6,70 @@ // import os.log -import Foundation +import UIKit +import Combine import WebKit -final class MastodonPinBasedAuthenticationViewController: NSObject { +final class MastodonPinBasedAuthenticationViewController: UIViewController, NeedsDependency { + weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } + weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - weak var viewModel: MastodonPinBasedAuthenticationViewModel? + var disposeBag = Set() + var viewModel: MastodonPinBasedAuthenticationViewModel! - init(viewModel: MastodonPinBasedAuthenticationViewModel) { - self.viewModel = viewModel - } + let webView: WKWebView = { + let configuration = WKWebViewConfiguration() + configuration.processPool = WKProcessPool() + let webView = WKWebView(frame: .zero, configuration: configuration) + return webView + }() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + + // cleanup cookie + let httpCookieStore = webView.configuration.websiteDataStore.httpCookieStore + httpCookieStore.getAllCookies { cookies in + for cookie in cookies { + httpCookieStore.delete(cookie, completionHandler: nil) + } + } + } + +} + + + +extension MastodonPinBasedAuthenticationViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Authentication" + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(MastodonPinBasedAuthenticationViewController.cancelBarButtonItemPressed(_:))) + + webView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(webView) + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: view.topAnchor), + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + let request = URLRequest(url: viewModel.authenticateURL) + webView.navigationDelegate = viewModel.navigationDelegate + webView.load(request) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: authenticate via: %s", ((#file as NSString).lastPathComponent), #line, #function, viewModel.authenticateURL.debugDescription) + } + +} + +extension MastodonPinBasedAuthenticationViewController { + + @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { + dismiss(animated: true, completion: nil) } } diff --git a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift index 4ae5e14a4..5eac359eb 100644 --- a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift +++ b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModel.swift @@ -5,8 +5,36 @@ // Created by MainasuK Cirno on 2021/1/29. // +import os.log import Foundation +import Combine +import WebKit final class MastodonPinBasedAuthenticationViewModel { + // input + let authenticateURL: URL + + // output + let pinCodePublisher = PassthroughSubject() + private var navigationDelegateShim: MastodonPinBasedAuthenticationViewModelNavigationDelegateShim? + + init(authenticateURL: URL) { + self.authenticateURL = authenticateURL + } + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension MastodonPinBasedAuthenticationViewModel { + + var navigationDelegate: WKNavigationDelegate { + let navigationDelegateShim = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim(viewModel: self) + self.navigationDelegateShim = navigationDelegateShim + return navigationDelegateShim + } + } diff --git a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift index 3f7092b1f..dd8901721 100644 --- a/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift +++ b/Mastodon/Scene/Authentication/PinBased/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift @@ -27,7 +27,14 @@ final class MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: NSObj extension MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: WKNavigationDelegate { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - // TODO: + guard let url = webView.url, + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }), + let code = codeQueryItem.value else { + return + } + + viewModel?.pinCodePublisher.send(code) } } diff --git a/Mastodon/Service/APIService+APIError.swift b/Mastodon/Service/APIService+APIError.swift new file mode 100644 index 000000000..2bb56cf2c --- /dev/null +++ b/Mastodon/Service/APIService+APIError.swift @@ -0,0 +1,29 @@ +// +// APIService+Error.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-2-2. +// + +import UIKit +import MastodonSDK + +extension APIService { + enum APIError: Error { + + case implicit(ErrorReason) + case explicit(ErrorReason) + + enum ErrorReason { + // application internal error + case authenticationMissing + case badRequest + case badResponse + case requestThrottle + + // Server API error + case mastodonAPIError(Mastodon.API.Error) + } + + } +} diff --git a/Mastodon/Service/APIService+Account.swift b/Mastodon/Service/APIService+Account.swift new file mode 100644 index 000000000..1a9ad08b5 --- /dev/null +++ b/Mastodon/Service/APIService+Account.swift @@ -0,0 +1,24 @@ +// +// APIService+Account.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/2/2. +// + +import Foundation +import Combine +import MastodonSDK + +extension APIService { + + func accountVerifyCredentials( + domain: String, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + return Mastodon.API.Account.verifyCredentials( + session: session, + domain: domain, + authorization: authorization + ) + } +} diff --git a/Mastodon/Service/APIService+App.swift b/Mastodon/Service/APIService+App.swift new file mode 100644 index 000000000..9726a6ee6 --- /dev/null +++ b/Mastodon/Service/APIService+App.swift @@ -0,0 +1,32 @@ +// +// APIService+App.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/2/2. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +extension APIService { + + #if DEBUG + private static let clientName = "Skimming" + #else + private static let clientName = "Mastodon for iOS" + #endif + + func createApplication(domain: String) -> AnyPublisher, Error> { + let query = Mastodon.API.App.CreateQuery(clientName: APIService.clientName, website: nil) + return Mastodon.API.App.create( + session: session, + domain: domain, + query: query + ) + } + +} + diff --git a/Mastodon/Service/APIService+Authentication.swift b/Mastodon/Service/APIService+Authentication.swift new file mode 100644 index 000000000..486a27c44 --- /dev/null +++ b/Mastodon/Service/APIService+Authentication.swift @@ -0,0 +1,35 @@ +// +// APIService+Authentication.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021/2/2. +// + +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK + +extension APIService { + + func userAccessToken( + domain: String, + clientID: String, + clientSecret: String, + code: String + ) -> AnyPublisher, Error> { + let query = Mastodon.API.OAuth.AccessTokenQuery( + clientID: clientID, + clientSecret: clientSecret, + code: code, + grantType: "authorization_code" + ) + return Mastodon.API.OAuth.accessToken( + session: session, + domain: domain, + query: query + ) + } + +} diff --git a/Mastodon/Service/APIService+PublicTimeline.swift b/Mastodon/Service/APIService+PublicTimeline.swift index 6b73409cf..3e75d2461 100644 --- a/Mastodon/Service/APIService+PublicTimeline.swift +++ b/Mastodon/Service/APIService+PublicTimeline.swift @@ -26,7 +26,7 @@ extension APIService { domain: domain, query: Mastodon.API.Timeline.PublicTimelineQuery() ) - .flatMap { response -> AnyPublisher,Error> in + .flatMap { response -> AnyPublisher, Error> in return APIService.Persist.persistTimeline( domain: domain, managedObjectContext: self.backgroundManagedObjectContext, @@ -46,4 +46,5 @@ extension APIService { } .eraseToAnyPublisher() } + } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index 2cddc5873..6aeb17d17 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -27,7 +27,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { #if DEBUG DispatchQueue.main.async { - let authenticationViewModel = AuthenticationViewModel() + let authenticationViewModel = AuthenticationViewModel(context: appContext, coordinator: sceneCoordinator) sceneCoordinator.present(scene: .authentication(viewModel: authenticationViewModel), from: nil, transition: .modal(animated: false, completion: nil)) } #endif diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift new file mode 100644 index 000000000..4929b0bb9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -0,0 +1,34 @@ +// +// Mastodon+API+Account.swift +// +// +// Created by MainasuK Cirno on 2021/2/2. +// + +import Foundation +import Combine + +extension Mastodon.API.Account { + + static func verifyCredentialsEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain) + } + + public static func verifyCredentials( + session: URLSession, + domain: String, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: verifyCredentialsEndpointURL(domain: domain), + query: nil, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Account.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift index c25526ff6..fc64c10d0 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift @@ -14,6 +14,10 @@ extension Mastodon.API.OAuth { public struct Authorization { public let accessToken: String + + public init(accessToken: String) { + self.accessToken = accessToken + } } } @@ -23,6 +27,9 @@ extension Mastodon.API.OAuth { static func authorizeEndpointURL(domain: String) -> URL { return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("authorize") } + static func accessTokenEndpointURL(domain: String) -> URL { + return Mastodon.API.oauthEndpointURL(domain: domain).appendingPathComponent("token") + } /// Construct user authorize endpoint URL /// @@ -38,7 +45,7 @@ extension Mastodon.API.OAuth { /// - session: `URLSession` /// - domain: Mastodon instance domain. e.g. "example.com" /// - query: `AuthorizeQuery` - static func authorizeURL( + public static func authorizeURL( domain: String, query: AuthorizeQuery ) -> URL { @@ -51,27 +58,41 @@ extension Mastodon.API.OAuth { return url } -// static func authorize( -// session: URLSession, -// domain: String, -// query: AuthorizeQuery -// ) -> AnyPublisher, Error> { -// let request = Mastodon.API.post( -// url: authorizeEndpointURL(domain: domain), -// query: query, -// authorization: nil -// ) -// return session.dataTaskPublisher(for: request) -// .tryMap { data, response in -// let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response) -// return Mastodon.Response.Content(value: value, response: response) -// } -// .eraseToAnyPublisher() -// } + /// Obtain User Access Token + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/2/2 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/apps/oauth/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `AccessTokenQuery` + /// - Returns: `AnyPublisher` contains `Token` nested in the response + public static func accessToken( + session: URLSession, + domain: String, + query: AccessTokenQuery + ) -> AnyPublisher, Error> { + let request = Mastodon.API.post( + url: accessTokenEndpointURL(domain: domain), + query: query, + authorization: nil + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Token.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } } extension Mastodon.API.OAuth { + public struct AuthorizeQuery: GetQuery { public let forceLogin: String? @@ -106,7 +127,7 @@ extension Mastodon.API.OAuth { var items: [URLQueryItem] = [] forceLogin.flatMap { items.append(URLQueryItem(name: "force_login", value: $0)) } items.append(URLQueryItem(name: "response_type", value: responseType)) - items.append(URLQueryItem(name: "clientID", value: clientID)) + items.append(URLQueryItem(name: "client_id", value: clientID)) items.append(URLQueryItem(name: "redirect_uri", value: redirectURI)) scope.flatMap { items.append(URLQueryItem(name: "scope", value: $0)) } guard !items.isEmpty else { return nil } @@ -114,4 +135,46 @@ extension Mastodon.API.OAuth { } } + + public struct AccessTokenQuery: Codable, PostQuery { + public init( + clientID: String, + clientSecret: String, + redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", + scope: String? = "read write follow push", + code: String?, + grantType: String + ) { + self.clientID = clientID + self.clientSecret = clientSecret + self.redirectURI = redirectURI + self.scope = scope + self.code = code + self.grantType = grantType + } + + + public let clientID: String + public let clientSecret: String + public let redirectURI: String + public let scope: String? + public let code: String? + public let grantType: String + + enum CodingKeys: String, CodingKey { + case clientID = "client_id" + case clientSecret = "client_secret" + case redirectURI = "redirect_uri" + case scope + case code + case grantType = "grant_type" + + } + + var body: Data? { + return try? Mastodon.API.encoder.encode(self) + } + + } + } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index fcab0eb3e..283d6e1d5 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -37,17 +37,33 @@ extension Mastodon.API { let decoder = JSONDecoder() decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.custom { decoder throws -> Date in let container = try decoder.singleValueContainer() - let string = try container.decode(String.self) - - if let date = fractionalSecondsPreciseISO8601Formatter.date(from: string) { - return date - } - if let date = fullDatePreciseISO8601Formatter.date(from: string) { - return date + var logInfo = "" + do { + let string = try container.decode(String.self) + logInfo += string + + if let date = fractionalSecondsPreciseISO8601Formatter.date(from: string) { + return date + } + if let date = fullDatePreciseISO8601Formatter.date(from: string) { + return date + } + } catch { + // do nothing } - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)") + var numberValue = "" + do { + let number = try container.decode(Double.self) + logInfo += "\(number)" + + return Date(timeIntervalSince1970: number) + } catch { + // do nothing + } + + throw DecodingError.dataCorruptedError(in: container, debugDescription: "[Decoder] Invalid date: \(logInfo)") } return decoder @@ -66,6 +82,7 @@ extension Mastodon.API { } extension Mastodon.API { + public enum Account { } public enum App { } public enum OAuth { } public enum Timeline { } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Token.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Token.swift index 5f9d273c4..e7f18b518 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Token.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Token.swift @@ -21,5 +21,12 @@ extension Mastodon.Entity { public let tokenType: String public let scope: String public let createdAt: Date + + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case tokenType = "token_type" + case scope + case createdAt = "created_at" + } } }