Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -68,7 +51,7 @@ extension ATProtoKit {
request,
decodeTo: AppBskyLexicon.Actor.ProfileViewDetailedDefinition.self
)

return result
} catch {
throw error
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
91 changes: 91 additions & 0 deletions Sources/ATProtoKit/ATProtoKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(_ 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
)
}
}