From 2fc7aaf563725ec85d0fc3e5ea4e328f3792f210 Mon Sep 17 00:00:00 2001 From: Michael Freiwald Date: Wed, 4 Jun 2025 09:40:08 +0200 Subject: [PATCH] Make creating request simpler --- .../AppBskyActorGetProfileMethod.swift | 29 ++---- .../AppBskyActorGetProfilesMethod.swift | 27 +----- .../AppBskyFeedGetAuthorFeedMethod.swift | 52 +++-------- .../AppBskyFeedGetPostsMethod.swift | 31 ++----- .../AppBskyGraphGetFollowersMethod.swift | 37 ++------ Sources/ATProtoKit/ATProtoKit.swift | 91 +++++++++++++++++++ 6 files changed, 126 insertions(+), 141 deletions(-) diff --git a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfileMethod.swift b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfileMethod.swift index 7568140247..543c5f676f 100644 --- a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfileMethod.swift +++ b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfileMethod.swift @@ -35,29 +35,12 @@ extension ATProtoKit { public func getProfile( for actor: String ) async throws -> AppBskyLexicon.Actor.ProfileViewDetailedDefinition { - let authorizationValue = await prepareAuthorizationValue() - - guard self.pdsURL != "" else { - throw ATRequestPrepareError.emptyPDSURL - } - - guard let sessionURL = authorizationValue != nil ? try await self.getUserSession()?.serviceEndpoint.absoluteString : self.pdsURL, - let requestURL = URL(string: "\(sessionURL)/xrpc/app.bsky.actor.getProfile") else { - throw ATRequestPrepareError.invalidRequestURL - } - - let queryItems = [ - ("actor", actor) - ] - - let queryURL: URL - do { - queryURL = try apiClientService.setQueryItems( - for: requestURL, - with: queryItems - ) - + let (authorizationValue, sessionURL) = try await prepareAuthorization(rquiresAuth: false) + + let queryURL = try await prepareRequest(sessionURL: sessionURL, endpoint: "/xrpc/app.bsky.actor.getProfile") { queryItems in + addActor(actor, to: &queryItems) + } let request = apiClientService.createRequest( forRequest: queryURL, andMethod: .get, @@ -68,7 +51,7 @@ extension ATProtoKit { request, decodeTo: AppBskyLexicon.Actor.ProfileViewDetailedDefinition.self ) - + return result } catch { throw error diff --git a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfilesMethod.swift b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfilesMethod.swift index c2795eba15..cbedd872be 100644 --- a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfilesMethod.swift +++ b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyActorGetProfilesMethod.swift @@ -36,29 +36,12 @@ extension ATProtoKit { public func getProfiles( for actors: [String] ) async throws -> AppBskyLexicon.Actor.GetProfilesOutput { - let authorizationValue = await prepareAuthorizationValue() - - guard self.pdsURL != "" else { - throw ATRequestPrepareError.emptyPDSURL - } - - guard let sessionURL = authorizationValue != nil ? try await self.getUserSession()?.serviceEndpoint.absoluteString : self.pdsURL, - let requestURL = URL(string: "\(sessionURL)/xrpc/app.bsky.actor.getProfiles") else { - throw ATRequestPrepareError.invalidRequestURL - } - - var queryItems = [(String, String)]() - - let cappedActorsArray = actors.prefix(25) - queryItems += cappedActorsArray.map { ("actors", $0) } - - let queryURL: URL - do { - queryURL = try apiClientService.setQueryItems( - for: requestURL, - with: queryItems - ) + let (authorizationValue, sessionURL) = try await prepareAuthorization(rquiresAuth: false) + + let queryURL = try await prepareRequest(sessionURL: sessionURL, endpoint: "/xrpc/app.bsky.actor.getProfiles") { queryItems in + queryItems += actors.prefix(25).map { ("actors", $0) } + } let request = apiClientService.createRequest( forRequest: queryURL, diff --git a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetAuthorFeedMethod.swift b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetAuthorFeedMethod.swift index 1b1ed42fb9..22954c342c 100644 --- a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetAuthorFeedMethod.swift +++ b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetAuthorFeedMethod.swift @@ -44,53 +44,23 @@ extension ATProtoKit { postFilter: AppBskyLexicon.Feed.GetAuthorFeed.Filter? = .postsWithReplies, shouldIncludePins: Bool? = false ) async throws -> AppBskyLexicon.Feed.GetAuthorFeedOutput { - guard let session = try await self.getUserSession(), - let keychain = sessionConfiguration?.keychainProtocol else { - throw ATRequestPrepareError.missingActiveSession - } - - let accessToken = try await keychain.retrieveAccessToken() - let sessionURL = session.serviceEndpoint.absoluteString - - guard let requestURL = URL(string: "\(sessionURL)/xrpc/app.bsky.feed.getAuthorFeed") else { - throw ATRequestPrepareError.invalidRequestURL - } - - var queryItems = [(String, String)]() - - queryItems.append(("actor", actorDID)) - - if let limit { - let finalLimit = max(1, min(limit, 100)) - queryItems.append(("limit", "\(finalLimit)")) - } - - if let cursor { - queryItems.append(("cursor", cursor)) - } - - if let postFilter { - queryItems.append(("filter", "\(postFilter.rawValue)")) - } - - if let shouldIncludePins { - queryItems.append(("includePins", "\(shouldIncludePins)")) - } - - let queryURL: URL - do { - queryURL = try apiClientService.setQueryItems( - for: requestURL, - with: queryItems - ) - + let (authorizationValue, sessionURL) = try await prepareAuthorization(rquiresAuth: false) + + let queryURL = try await prepareRequest(sessionURL: sessionURL, endpoint: "/xrpc/app.bsky.feed.getAuthorFeed") { queryItems in + addQueryItem("actor", value: actorDID, to: &queryItems) + addLimit(limit, max: 100, to: &queryItems) + addCursor(cursor, to: &queryItems) + addQueryItem("filter", value: postFilter?.rawValue, to: &queryItems) + addQueryItem("includePins", value: shouldIncludePins, to: &queryItems) + } + let request = apiClientService.createRequest( forRequest: queryURL, andMethod: .get, acceptValue: "application/json", contentTypeValue: nil, - authorizationValue: "Bearer \(accessToken)" + authorizationValue: authorizationValue ) let response = try await apiClientService.sendRequest( request, diff --git a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetPostsMethod.swift b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetPostsMethod.swift index 760090e085..203fbd29d7 100644 --- a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetPostsMethod.swift +++ b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyFeedGetPostsMethod.swift @@ -30,38 +30,19 @@ extension ATProtoKit { /// - Throws: An ``ATProtoError``-conforming error type, depending on the issue. Go to /// ``ATAPIError`` and ``ATRequestPrepareError`` for more details. public func getPosts(_ uris: [String]) async throws -> AppBskyLexicon.Feed.GetPostsOutput { - guard let session = try await self.getUserSession(), - let keychain = sessionConfiguration?.keychainProtocol else { - throw ATRequestPrepareError.missingActiveSession - } - - let accessToken = try await keychain.retrieveAccessToken() - let sessionURL = session.serviceEndpoint.absoluteString - - guard let requestURL = URL(string: "\(sessionURL)/xrpc/app.bsky.feed.getPosts") else { - throw ATRequestPrepareError.invalidRequestURL - } - - var queryItems = [(String, String)]() - - // Cap the array to 25 items. - let cappedURIArray = uris.prefix(25) - queryItems += cappedURIArray.map { ("uris", $0) } - - let queryURL: URL - do { - queryURL = try apiClientService.setQueryItems( - for: requestURL, - with: queryItems - ) + let (authorizationValue, sessionURL) = try await prepareAuthorization(rquiresAuth: false) + + let queryURL = try await prepareRequest(sessionURL: sessionURL, endpoint: "/xrpc/app.bsky.feed.getPosts") { queryItems in + queryItems += uris.prefix(25).map { ("uris", $0) } + } let request = apiClientService.createRequest( forRequest: queryURL, andMethod: .get, acceptValue: "application/json", contentTypeValue: nil, - authorizationValue: "Bearer \(accessToken)" + authorizationValue: authorizationValue ) let response = try await apiClientService.sendRequest( request, diff --git a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyGraphGetFollowersMethod.swift b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyGraphGetFollowersMethod.swift index c34b4dd8ed..af7c22f1c4 100644 --- a/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyGraphGetFollowersMethod.swift +++ b/Sources/ATProtoKit/APIReference/AppBskyAPI/AppBskyGraphGetFollowersMethod.swift @@ -34,37 +34,14 @@ extension ATProtoKit { limit: Int? = 50, cursor: String? = nil ) async throws -> AppBskyLexicon.Graph.GetFollowersOutput { - let authorizationValue = await prepareAuthorizationValue() - - guard self.pdsURL != "" else { - throw ATRequestPrepareError.emptyPDSURL - } - - guard let sessionURL = authorizationValue != nil ? try await self.getUserSession()?.serviceEndpoint.absoluteString : self.pdsURL, - let requestURL = URL(string: "\(sessionURL)/xrpc/app.bsky.graph.getFollowers") else { - throw ATRequestPrepareError.invalidRequestURL - } - - var queryItems = [(String, String)]() - - queryItems.append(("actor", actorDID)) - - if let limit { - let finalLimit = max(1, min(limit, 100)) - queryItems.append(("limit", "\(finalLimit)")) - } - - if let cursor { - queryItems.append(("cursor", cursor)) - } - - let queryURL: URL - do { - queryURL = try apiClientService.setQueryItems( - for: requestURL, - with: queryItems - ) + let (authorizationValue, sessionURL) = try await prepareAuthorization(rquiresAuth: false) + + let queryURL = try await prepareRequest(sessionURL: sessionURL, endpoint: "/xrpc/app.bsky.graph.getFollowers") { queryItems in + addQueryItem("actor", value: actorDID, to: &queryItems) + addLimit(limit, to: &queryItems) + addCursor(cursor, to: &queryItems) + } let request = apiClientService.createRequest( forRequest: queryURL, diff --git a/Sources/ATProtoKit/ATProtoKit.swift b/Sources/ATProtoKit/ATProtoKit.swift index 8d02998244..e31259dd2e 100644 --- a/Sources/ATProtoKit/ATProtoKit.swift +++ b/Sources/ATProtoKit/ATProtoKit.swift @@ -369,3 +369,94 @@ public final class ATProtoAdmin: Sendable, ATProtoKitConfiguration { ) } } + +extension ATProtoKit { + /// Prepares the necessary authorization credentials and session URL for making requests. + /// + /// This method checks whether authorization is required and returns the appropriate access token and session URL. + /// - If `rquiresAuth` is `true`, it retrieves the current user session and access token from the keychain. + /// - If `rquiresAuth` is `false`, it attempts to prepare a fallback authorization value and uses the `pdsURL` if available. + /// + /// - Parameter rquiresAuth: A Boolean value indicating whether authorization is required for the request. + /// - Returns: A tuple containing an optional access token and the session URL as a `String`. + /// - Throws: + /// - `ATRequestPrepareError.missingActiveSession` if an active session or keychain is missing when authorization is required. + /// - `ATRequestPrepareError.emptyPDSURL` if the `pdsURL` is empty when authorization is not required. + /// - `ATRequestPrepareError.invalidRequestURL` if a valid request URL cannot be constructed. + /// - Note: This method is asynchronous and may perform network or keychain access. + /// - Important: Ensure that `sessionConfiguration` and `pdsURL` are properly set before calling this method. + /// + /// Example usage: + /// ```swift + /// let (accessToken, sessionURL) = try await kit.prepareAuthorization(rquiresAuth: true) + /// ``` + public func prepareAuthorization(rquiresAuth: Bool) async throws -> (accessToken: String?, sessionURL: String) { + if rquiresAuth { + guard let session = try await self.getUserSession(), + let keychain = sessionConfiguration?.keychainProtocol else { + throw ATRequestPrepareError.missingActiveSession + } + + let accessToken = try await keychain.retrieveAccessToken() + let sessionURL = session.serviceEndpoint.absoluteString + + return (accessToken, sessionURL) + } else { + let authorizationValue = await prepareAuthorizationValue() + + guard self.pdsURL != "" else { + throw ATRequestPrepareError.emptyPDSURL + } + + guard let sessionURL = authorizationValue != nil ? try await self.getUserSession()?.serviceEndpoint.absoluteString : self.pdsURL else { + throw ATRequestPrepareError.invalidRequestURL + } + + return (nil, sessionURL) + } + } + + func prepareRequest(sessionURL: String, endpoint: String, _ prepareQueryItems: (inout [(String, String)]) -> Void) async throws -> URL { + guard let requestURL = URL(string: "\(sessionURL)\(endpoint)") else { + throw ATRequestPrepareError.invalidRequestURL + } + + var queryItems: [(String, String)] = [] + prepareQueryItems(&queryItems) + + let queryURL = try queryURL( + for: requestURL, + with: queryItems + ) + + return queryURL + } + + func addQueryItem(_ name: String, value: T?, to queryItems: inout [(String, String)]) { + if let value { + queryItems.append((name, "\(value)")) + } + } + + func addActor(_ actor: String?, to queryItems: inout [(String, String)]) { + addQueryItem("actor", value: actor, to: &queryItems) + } + + func addCursor(_ cursor: String?, to queryItems: inout [(String, String)]) { + addQueryItem("cursor", value: cursor, to: &queryItems) + } + + func addLimit(_ limit: Int?, max maxLimit: Int = 100, to queryItems: inout [(String, String)]) { + if let limit { + let finalLimit = max(1, min(limit, maxLimit)) + addQueryItem("limit", value: "\(finalLimit)", to: &queryItems) + } + } + + func queryURL(for requestURL: URL, with queryItems: [(String, String)]) throws -> URL { + try apiClientService.setQueryItems( + for: requestURL, + with: queryItems + ) + } +}