Skip to content

Commit

Permalink
Merge pull request #98 from makinosp/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
makinosp authored Nov 2, 2024
2 parents b4f4550 + 7c88c46 commit c9b0a05
Show file tree
Hide file tree
Showing 18 changed files with 148 additions and 89 deletions.
6 changes: 3 additions & 3 deletions Sources/VRCKit/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public final actor APIClient {
/// - Throws: `VRCKitError.unexpectedError` if the username and password cannot be converted to UTF-8 data.
private func encodeAuthorization(_ credential: Credential) throws -> String {
guard let payload = credential.authString.data(using: .utf8) else {
throw VRCKitError.unexpected
throw VRCKitError.credentialNotSet
}
return "Basic \(payload.base64EncodedString())"
}
Expand Down Expand Up @@ -108,7 +108,7 @@ public final actor APIClient {
return
}
guard let data = data, let reponse = urlResponse as? HTTPURLResponse else {
continuation.resume(throwing: VRCKitError.invalidResponse)
continuation.resume(throwing: VRCKitError.invalidResponse(String(describing: data)))
return
}
continuation.resume(returning: (data, reponse))
Expand All @@ -121,7 +121,7 @@ public final actor APIClient {
private func requestWithFoundation(_ request: URLRequest) async throws -> HTTPResponse {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw VRCKitError.invalidResponse
throw VRCKitError.invalidResponse(String(describing: data))
}
return (data, response)
}
Expand Down
18 changes: 9 additions & 9 deletions Sources/VRCKit/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ public enum VRCKitError: Error, LocalizedError, Equatable {
/// Represents an error indicating that the client has been deallocated.
case clientDeallocated

/// Represents an error indicating that credential not set.
case credentialNotSet

/// Represents an error indicating an invalid response was received.
case invalidResponse
case invalidResponse(_ details: String)

/// Represents an error indicating an invalid request with additional details.
case invalidRequest(_ details: String)

/// Represents an error indicating an authentication failure.
case unauthorized

/// Represents an unexpected error.
case unexpected

/// Represents an url error.
case urlError

Expand All @@ -42,21 +42,21 @@ public enum VRCKitError: Error, LocalizedError, Equatable {
case .apiError: "API Error"
case .badGateway: "Bad Gateway"
case .clientDeallocated: "Client Deallocated"
case .credentialNotSet: "Credential Error"
case .invalidResponse: "Invalid Response"
case .invalidRequest: "Invalid Request"
case .unauthorized: "Unauthorized"
case .unexpected: "Unexpected"
case .urlError: "URL Error"
}
}

/// Provides a localized failure reason for the error.
public var failureReason: String? {
switch self {
case .apiError(let details), .invalidRequest(let details):
details
default:
errorDescription
case .apiError(let details): details
case .invalidRequest(let details): details
case .invalidResponse(let details): details
default: errorDescription
}
}
}
20 changes: 20 additions & 0 deletions Sources/VRCKit/Protocols/AuthenticationServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,29 @@
//

public protocol AuthenticationServiceProtocol: Sendable {

/// Check if a user exists by their user ID.
/// - Parameter userId: The ID of the user to check.
/// - Returns: A boolean indicating if the user exists.
func exists(userId: String) async throws -> Bool

/// Fetches the authenticated user's information or determines if two-factor authentication (2FA) verification is required.
/// - Returns: An `Either<User, VerifyType>` result, which is `.left(User)` if the user is successfully authenticated,
/// - Throws: An error if the request fails or if the response cannot be decoded. If an unexpected decoding issue occurs,
/// it throws `VRCKitError.unexpected`.
func loginUserInfo() async throws -> Either<User, VerifyType>

/// Verifies 2-factor authentication using either TOTP or Email OTP.
/// - Parameters:
/// - verifyType: The type of verification (TOTP or Email OTP).
/// - code: The 6-digit verification code.
/// - Returns: A boolean indicating if verification was successful.
func verify2FA(verifyType: VerifyType, code: String) async throws -> Bool

/// Verifies the user's authentication token.
/// - Returns: A boolean indicating if the token is valid.
func verifyAuthToken() async throws -> Bool

/// Logs out the user and deletes cookies.
func logout() async throws
}
38 changes: 38 additions & 0 deletions Sources/VRCKit/Protocols/FavoriteServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,52 @@
//

public protocol FavoriteServiceProtocol: Sendable {
/// Asynchronously retrieves a list of favorite groups from the server.
/// - Returns: An array of `FavoriteGroup` objects.
func listFavoriteGroups() async throws -> [FavoriteGroup]

/// Lists a user's all favorites with the specified parameters.
/// - Parameter type: The type of favorite (e.g., friend, world).
/// - Returns: An array of `Favorite` objects.
func listFavorites(type: FavoriteType) async throws -> [Favorite]

/// Lists a user's favorites with the specified parameters.
/// - Parameters:
/// - n: The number of favorites to retrieve. Default is `60`.
/// - offset: Offset value of favorites to retrive. Default is `0`.
/// - type: The type of favorite (e.g., friend, world).
/// - tag: An optional tag to filter favorites.
/// - Returns: An array of `Favorite` objects.
func listFavorites(n: Int, offset: Int, type: FavoriteType, tag: String?) async throws -> [Favorite]

/// Fetches details of favorite groups asynchronously.
/// - Parameters:
/// - favoriteGroups: An array of `FavoriteGroup` objects.
/// - type: The type of favorite (e.g., friend, world).
/// - Returns: An array of `FavoriteList` objects containing detailed information about the favorite groups.
func fetchFavoriteList(favoriteGroups: [FavoriteGroup], type: FavoriteType) async throws -> [FavoriteList]

/// Adds a new favorite to a specific group.
/// - Parameters:
/// - type: The type of favorite (e.g., friend, world).
/// - favoriteId: The ID of the item to favorite.
/// - tag: The tag to associate with the favorite.
/// - Returns: The newly added `Favorite` object.
func addFavorite(type: FavoriteType, favoriteId: String, tag: String) async throws -> Favorite

/// Updates a favorite group with the given parameters, display name, and visibility.
/// - Parameters:
/// - source: An object containing the favorite group's details.
/// - displayName: The new display name to update the favorite group with.
/// - visibility: The new visibility setting for the favorite group.
func updateFavoriteGroup(
source: FavoriteGroup,
displayName: String,
visibility: FavoriteGroup.Visibility
) async throws

/// Asynchronously remove favorite.
/// - Parameter favoriteId: The ID of the favorite to remove.
/// - Returns: A `SuccessResponse` objects.
func removeFavorite(favoriteId: String) async throws -> SuccessResponse
}
18 changes: 18 additions & 0 deletions Sources/VRCKit/Protocols/FriendServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,26 @@
//

public protocol FriendServiceProtocol: Sendable {
/// Fetches a list of friends with pagination support.
/// - Parameters:
/// - offset: The offset to start retrieving friends from.
/// - n: The maximum number of friends to retrieve in the request (default is 60).
/// - offline: A Boolean indicating whether to include offline friends.
/// - Returns: An array of `Friend` objects representing the user's friends.
/// - Throws: An error if the request fails or decoding is unsuccessful.
func fetchFriends(offset: Int, n: Int, offline: Bool) async throws -> [Friend]

/// Fetches a list of friends up to a specified count, using multiple requests if needed.
/// - Parameters:
/// - count: The total number of friends to retrieve.
/// - offline: A Boolean indicating whether to include offline friends.
/// - Returns: An array of `Friend` objects representing the user's friends.
/// - Throws: An error if any request fails or decoding is unsuccessful.
func fetchFriends(count: Int, offline: Bool) async throws -> [Friend]

/// Removes a friend with the specified ID.
/// - Parameter id: The ID of the friend to be removed.
/// - Throws: An error if the request fails.
func unfriend(id: String) async throws
}

Expand Down
7 changes: 7 additions & 0 deletions Sources/VRCKit/Protocols/ImageUrlRepresentableProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
import Foundation

public protocol ImageUrlRepresentable: Sendable {
/// Replaces the last numeric component in the provided image URL with a specified resolution and returns a new URL.
///
/// - Parameters:
/// - url: The original image URL to modify.
/// - resolution: The desired image resolution as an `ImageResolution` value. The original resolution is `.origin`.
///
/// - Returns: A new URL with the last numeric component replaced by the specified resolution.
func imageUrl(_ resolution: ImageResolution) -> URL?
}

Expand Down
11 changes: 11 additions & 0 deletions Sources/VRCKit/Protocols/InstanceServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@
//

public protocol InstanceServiceProtocol: Sendable {
/// Fetches an instance of a world using the specified world ID and instance ID.
/// - Parameters:
/// - worldId: The ID of the world to fetch the instance from.
/// - instanceId: The ID of the instance to fetch.
/// - Returns: An `Instance` object representing the fetched instance.
/// - Throws: An error if the request fails or the data cannot be decoded.
func fetchInstance(worldId: String, instanceId: String) async throws -> Instance

/// Fetches an instance using the specified location string.
/// - Parameter location: The location string in the format "worldId:instanceId".
/// - Returns: An `Instance` object representing the fetched instance.
/// - Throws: An error if the request fails or the data cannot be decoded.
func fetchInstance(location: String) async throws -> Instance
}
8 changes: 8 additions & 0 deletions Sources/VRCKit/Protocols/UserNoteServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
//

public protocol UserNoteServiceProtocol: Sendable {
/// Updates the note for a specific user by sending the note to the API.
/// - Parameters:
/// - targetUserId: The ID of the user for whom the note is being updated.
/// - note: The content of the note to be added or updated.
/// - Returns: A `UserNoteResponse` containing the updated note information.
func updateUserNote(targetUserId: String, note: String) async throws -> UserNoteResponse

/// Clears the note for a specific user by sending an empty note to the API.
/// - Parameter targetUserId: The ID of the user whose note is being cleared.
func clearUserNote(targetUserId: String) async throws
}
10 changes: 10 additions & 0 deletions Sources/VRCKit/Protocols/UserServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@
//

public protocol UserServiceProtocol: Sendable {
/// Fetches detailed information about a specific user.
/// - Parameter userId: The ID of the user to retrieve.
/// - Returns: A `UserDetail` object containing detailed information about the specified user.
/// - Throws: An error if the request fails or decoding is unsuccessful.
func fetchUser(userId: String) async throws -> UserDetail

/// Updates the information for a specific user.
/// - Parameters:
/// - id: The ID of the user to update.
/// - editedInfo: An `EditableUserInfo` object containing the updated user information.
/// - Throws: An error if the request fails or encoding is unsuccessful.
func updateUser(id: String, editedInfo: EditableUserInfo) async throws
}
17 changes: 17 additions & 0 deletions Sources/VRCKit/Protocols/WorldServiceProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,23 @@
//

public protocol WorldServiceProtocol: Sendable {
/// Fetches detailed information about a specific world.
/// - Parameter worldId: The ID of the world to retrieve.
/// - Returns: A `World` object containing the details of the specified world.
/// - Throws: An error if the request fails or decoding is unsuccessful.
func fetchWorld(worldId: String) async throws -> World

/// Retrieves the list of favorited worlds.
/// - Returns: An array of `FavoriteWorld` objects representing the user's favorited worlds.
/// - Throws: An error if any request fails or decoding is unsuccessful.
func fetchFavoritedWorlds() async throws -> [FavoriteWorld]

/// Fetches a paginated list of favorited worlds.
/// This function retrieves a subset of favorited worlds based on the specified limit and offset.
/// - Parameters:
/// - n: The maximum number of worlds to retrieve in the current request.
/// - offset: The offset used to paginate the results.
/// - Returns: An array of `FavoriteWorld` objects representing a subset of favorited worlds.
/// - Throws: An error if the request fails or decoding is unsuccessful.
func fetchFavoritedWorlds(n: Int, offset: Int) async throws -> [FavoriteWorld]
}
15 changes: 1 addition & 14 deletions Sources/VRCKit/Services/AuthenticationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto
public let client: APIClient
private let authPath = "auth"

/// Check if a user exists by their user ID.
/// - Parameter userId: The ID of the user to check.
/// - Returns: A boolean indicating if the user exists.
public func exists(userId: String) async throws -> Bool {
let path = "\(authPath)/exists"
let queryItems = [URLQueryItem(name: "username", value: userId.description)]
Expand All @@ -24,8 +21,6 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto
return result.userExists
}

/// Logs in and/or fetches the current user's information.
/// - Returns: A `User` object or a `RequiresTwoFactorAuthResponse` if 2FA is required.
public func loginUserInfo() async throws -> Either<User, VerifyType> {
let path = "\(authPath)/user"
let response = try await client.request(path: path, method: .get, basic: true)
Expand All @@ -35,17 +30,12 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto
} catch _ as DecodingError {
let result: RequiresTwoFactorAuthResponse = try Serializer.shared.decode(response.data)
guard let requires = result.requires else {
throw VRCKitError.unexpected
throw VRCKitError.invalidResponse("\(result.requires?.rawValue)")
}
return .right(requires)
}
}

/// Verifies 2-factor authentication using either TOTP or Email OTP.
/// - Parameters:
/// - verifyType: The type of verification (TOTP or Email OTP).
/// - code: The 6-digit verification code.
/// - Returns: A boolean indicating if verification was successful.
public func verify2FA(verifyType: VerifyType, code: String) async throws -> Bool {
guard code.count == 6 else { throw VRCKitError.invalidRequest("Code must be 6 digits") }
let path = "\(authPath)/twofactorauth/\(verifyType.rawValue.lowercased())/verify"
Expand All @@ -59,15 +49,12 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto
return result.verified
}

/// Verifies the user's authentication token.
/// - Returns: A boolean indicating if the token is valid.
public func verifyAuthToken() async throws -> Bool {
let response = try await client.request(path: authPath, method: .get)
let result: VerifyAuthTokenResponse = try Serializer.shared.decode(response.data)
return result.ok
}

/// Logs out the user and deletes cookies.
public func logout() async throws {
_ = try await client.request(path: "logout", method: .put)
await client.cookieManager.deleteCookies()
Expand Down
Loading

0 comments on commit c9b0a05

Please sign in to comment.