From 769aa92b9e471cd34e6f2d966c87960d16cec70b Mon Sep 17 00:00:00 2001 From: makinosp Date: Mon, 21 Oct 2024 22:43:36 +0900 Subject: [PATCH 1/6] feat: Instance.Region adopt to CaseIterable --- Sources/VRCKit/Models/World/InstanceModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/VRCKit/Models/World/InstanceModel.swift b/Sources/VRCKit/Models/World/InstanceModel.swift index 3228ace..a2094ef 100644 --- a/Sources/VRCKit/Models/World/InstanceModel.swift +++ b/Sources/VRCKit/Models/World/InstanceModel.swift @@ -39,7 +39,7 @@ public struct Instance: Sendable, Identifiable, Hashable, Decodable { case `public`, plus } - public enum Region: String, Sendable, Codable { + public enum Region: String, Sendable, Codable, CaseIterable { case us, use, eu, jp, unknown } From 94cf3114e97353a9302339cd073b3f1468c6e1ee Mon Sep 17 00:00:00 2001 From: makinosp Date: Mon, 21 Oct 2024 22:44:00 +0900 Subject: [PATCH 2/6] refact: default value of Platforms --- Sources/VRCKit/Models/World/InstanceModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/VRCKit/Models/World/InstanceModel.swift b/Sources/VRCKit/Models/World/InstanceModel.swift index a2094ef..15a88bb 100644 --- a/Sources/VRCKit/Models/World/InstanceModel.swift +++ b/Sources/VRCKit/Models/World/InstanceModel.swift @@ -30,9 +30,9 @@ public struct Instance: Sendable, Identifiable, Hashable, Decodable { @MemberwiseInit(.public) public struct Platforms: Sendable, Hashable, Codable { - public let android: Int - public let ios: Int - public let standalonewindows: Int + @Init(default: 0) public let android: Int + @Init(default: 0) public let ios: Int + @Init(default: 0) public let standalonewindows: Int } public enum GroupAccessType: String, Sendable, Codable { From eb00f73ad61c51b01f2efe6307bbb4608d213a76 Mon Sep 17 00:00:00 2001 From: makinosp Date: Sun, 27 Oct 2024 12:15:02 +0900 Subject: [PATCH 3/6] feat: change Serializer from actor to Sendable class --- Sources/VRCKit/Utils/Serializer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/VRCKit/Utils/Serializer.swift b/Sources/VRCKit/Utils/Serializer.swift index 3819868..451ac6c 100644 --- a/Sources/VRCKit/Utils/Serializer.swift +++ b/Sources/VRCKit/Utils/Serializer.swift @@ -7,7 +7,7 @@ import Foundation -final actor Serializer { +final class Serializer: Sendable { static let shared = Serializer() private let decoder: JSONDecoder private let encoder: JSONEncoder From de0bbd83df093595c5dcdc121d6d942f4ce15bda Mon Sep 17 00:00:00 2001 From: makinosp Date: Sun, 27 Oct 2024 12:16:55 +0900 Subject: [PATCH 4/6] refact: refactoring CookieManager --- Sources/VRCKit/Utils/CookieManager.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/VRCKit/Utils/CookieManager.swift b/Sources/VRCKit/Utils/CookieManager.swift index 97ff5d4..e477f62 100644 --- a/Sources/VRCKit/Utils/CookieManager.swift +++ b/Sources/VRCKit/Utils/CookieManager.swift @@ -9,19 +9,16 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif +import MemberwiseInit +@MemberwiseInit public final actor CookieManager { - private var domainURL: String? - - init(domainURL: String) { - self.domainURL = domainURL - } + @Init(.internal) private var domainURL: String /// Retrieves the cookies stored for the VRChat API domain. /// - Returns: An array of `HTTPCookie` objects. public var cookies: [HTTPCookie] { - guard let domainURL = domainURL, - let url = URL(string: domainURL), + guard let url = URL(string: domainURL), let cookies = HTTPCookieStorage.shared.cookies(for: url) else { return [] } return cookies } From ec6e62b3fd1f93c8ebc7d0d99232784abab8f8ee Mon Sep 17 00:00:00 2001 From: makinosp Date: Sun, 27 Oct 2024 12:43:56 +0900 Subject: [PATCH 5/6] refact: remove unnecessary awaiting --- Sources/VRCKit/Services/AuthenticationService.swift | 12 ++++++------ Sources/VRCKit/Services/FavoriteService.swift | 12 ++++++------ Sources/VRCKit/Services/FriendService.swift | 2 +- Sources/VRCKit/Services/InstanceService.swift | 4 ++-- Sources/VRCKit/Services/UserNoteService.swift | 4 ++-- Sources/VRCKit/Services/UserService.swift | 4 ++-- Sources/VRCKit/Services/WorldService.swift | 4 ++-- 7 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Sources/VRCKit/Services/AuthenticationService.swift b/Sources/VRCKit/Services/AuthenticationService.swift index 93f94da..6d2a2c0 100644 --- a/Sources/VRCKit/Services/AuthenticationService.swift +++ b/Sources/VRCKit/Services/AuthenticationService.swift @@ -20,7 +20,7 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto let path = "\(authPath)/exists" let queryItems = [URLQueryItem(name: "username", value: userId.description)] let response = try await client.request(path: path, method: .get, queryItems: queryItems) - let result: ExistsResponse = try await Serializer.shared.decode(response.data) + let result: ExistsResponse = try Serializer.shared.decode(response.data) return result.userExists } @@ -30,10 +30,10 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto let path = "\(authPath)/user" let response = try await client.request(path: path, method: .get, basic: true) do { - let user: User = try await Serializer.shared.decode(response.data) + let user: User = try Serializer.shared.decode(response.data) return .left(user) } catch _ as DecodingError { - let result: RequiresTwoFactorAuthResponse = try await Serializer.shared.decode(response.data) + let result: RequiresTwoFactorAuthResponse = try Serializer.shared.decode(response.data) guard let requires = result.requires else { throw VRCKitError.unexpected } @@ -49,13 +49,13 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto 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" - let requestData = try await Serializer.shared.encode(VerifyRequest(code: code)) + let requestData = try Serializer.shared.encode(VerifyRequest(code: code)) let response = try await client.request( path: path, method: .post, body: requestData ) - let result: VerifyResponse = try await Serializer.shared.decode(response.data) + let result: VerifyResponse = try Serializer.shared.decode(response.data) return result.verified } @@ -63,7 +63,7 @@ public final actor AuthenticationService: APIService, AuthenticationServiceProto /// - 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 await Serializer.shared.decode(response.data) + let result: VerifyAuthTokenResponse = try Serializer.shared.decode(response.data) return result.ok } diff --git a/Sources/VRCKit/Services/FavoriteService.swift b/Sources/VRCKit/Services/FavoriteService.swift index 3818eed..13dbe5b 100644 --- a/Sources/VRCKit/Services/FavoriteService.swift +++ b/Sources/VRCKit/Services/FavoriteService.swift @@ -19,7 +19,7 @@ public final actor FavoriteService: APIService, FavoriteServiceProtocol { public func listFavoriteGroups() async throws -> [FavoriteGroup] { let path = "favorite/groups" let response = try await client.request(path: path, method: .get) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } /// Lists a user's all favorites with the specified parameters. @@ -63,7 +63,7 @@ public final actor FavoriteService: APIService, FavoriteServiceProtocol { queryItems.append(URLQueryItem(name: "tag", value: tag.description)) } let response = try await client.request(path: path, method: .get, queryItems: queryItems) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } /// Fetches details of favorite groups asynchronously. @@ -100,11 +100,11 @@ public final actor FavoriteService: APIService, FavoriteServiceProtocol { tag: String ) async throws -> Favorite { let path = "favorites" - let requestData = try await Serializer.shared.encode( + let requestData = try Serializer.shared.encode( RequestToAddFavorite(type: type, favoriteId: favoriteId, tags: [tag]) ) let response = try await client.request(path: path, method: .post, body: requestData) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } /// Updates a favorite group with the given parameters, display name, and visibility. @@ -123,7 +123,7 @@ public final actor FavoriteService: APIService, FavoriteServiceProtocol { let pathParams = ["favorite", "group", source.type.rawValue, source.name, source.ownerId] let path = pathParams.joined(separator: "/") let body = RequestToUpdateFavoriteGroup(displayName: displayName, visibility: visibility) - let requestData = try await Serializer.shared.encode(body) + let requestData = try Serializer.shared.encode(body) _ = try await client.request(path: path, method: .put, body: requestData) } @@ -133,6 +133,6 @@ public final actor FavoriteService: APIService, FavoriteServiceProtocol { public func removeFavorite(favoriteId: String) async throws -> SuccessResponse { let path = "favorites/\(favoriteId)" let response = try await client.request(path: path, method: .delete) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } } diff --git a/Sources/VRCKit/Services/FriendService.swift b/Sources/VRCKit/Services/FriendService.swift index 77444ce..12f0b9d 100644 --- a/Sources/VRCKit/Services/FriendService.swift +++ b/Sources/VRCKit/Services/FriendService.swift @@ -21,7 +21,7 @@ public final actor FriendService: APIService, FriendServiceProtocol { URLQueryItem(name: "offline", value: offline.description) ] let response = try await client.request(path: path, method: .get, queryItems: queryItems) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } /// A helper function that splits a large API request tasks to fetch friend data concurrently, diff --git a/Sources/VRCKit/Services/InstanceService.swift b/Sources/VRCKit/Services/InstanceService.swift index 8f07f6b..546db24 100644 --- a/Sources/VRCKit/Services/InstanceService.swift +++ b/Sources/VRCKit/Services/InstanceService.swift @@ -23,7 +23,7 @@ public final actor InstanceService: APIService, InstanceServiceProtocol { path: "\(path)/\(worldId):\(instanceId)", method: .get ) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } /// Fetches an instance using the specified location string. @@ -32,6 +32,6 @@ public final actor InstanceService: APIService, InstanceServiceProtocol { /// - Throws: An error if the request fails or the data cannot be decoded. public func fetchInstance(location: String) async throws -> Instance { let response = try await client.request(path: "\(path)/\(location)", method: .get) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } } diff --git a/Sources/VRCKit/Services/UserNoteService.swift b/Sources/VRCKit/Services/UserNoteService.swift index 5923767..2264051 100644 --- a/Sources/VRCKit/Services/UserNoteService.swift +++ b/Sources/VRCKit/Services/UserNoteService.swift @@ -24,7 +24,7 @@ public final actor UserNoteService: APIService, UserNoteServiceProtocol { let response = try await request( userNote: UserNoteRequest(targetUserId: targetUserId, note: note) ) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } /// Clears the note for a specific user by sending an empty note to the API. @@ -37,7 +37,7 @@ public final actor UserNoteService: APIService, UserNoteServiceProtocol { /// - Parameter userNote: The `UserNoteRequest` containing the user ID and note content. /// - Returns: The `HTTPResponse` received from the API. private func request(userNote: UserNoteRequest) async throws -> APIClient.HTTPResponse { - let requestData = try await Serializer.shared.encode(userNote) + let requestData = try Serializer.shared.encode(userNote) return try await client.request(path: path, method: .post, body: requestData) } } diff --git a/Sources/VRCKit/Services/UserService.swift b/Sources/VRCKit/Services/UserService.swift index 6b1c12a..285f442 100644 --- a/Sources/VRCKit/Services/UserService.swift +++ b/Sources/VRCKit/Services/UserService.swift @@ -15,12 +15,12 @@ public final actor UserService: APIService, UserServiceProtocol { /// Fetch a user public func fetchUser(userId: String) async throws -> UserDetail { let response = try await client.request(path: "\(path)/\(userId)", method: .get) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } /// Update user public func updateUser(id: String, editedInfo: EditableUserInfo) async throws { - let requestData = try await Serializer.shared.encode(editedInfo) + let requestData = try Serializer.shared.encode(editedInfo) _ = try await client.request( path: "\(path)/\(id)", method: .put, diff --git a/Sources/VRCKit/Services/WorldService.swift b/Sources/VRCKit/Services/WorldService.swift index ebf6bf9..0873078 100644 --- a/Sources/VRCKit/Services/WorldService.swift +++ b/Sources/VRCKit/Services/WorldService.swift @@ -18,7 +18,7 @@ public final actor WorldService: APIService, WorldServiceProtocol { public func fetchWorld(worldId: String) async throws -> World { let response = try await client.request(path: "\(path)/\(worldId)", method: .get) - return try await Serializer.shared.decode(response.data) + return try Serializer.shared.decode(response.data) } public func fetchFavoritedWorlds() async throws -> [FavoriteWorld] { @@ -44,7 +44,7 @@ public final actor WorldService: APIService, WorldServiceProtocol { URLQueryItem(name: "offset", value: offset.description) ] let response = try await client.request(path: "\(path)/favorites", method: .get, queryItems: queryItems) - let favoriteWorldWrapper: SafeDecodingArray = try await Serializer.shared.decode(response.data) + let favoriteWorldWrapper: SafeDecodingArray = try Serializer.shared.decode(response.data) return favoriteWorldWrapper.wrappedValue } } From fab9dbea46bb5dc755eb815a9a9529ff971949cb Mon Sep 17 00:00:00 2001 From: makinosp Date: Sun, 27 Oct 2024 16:00:02 +0900 Subject: [PATCH 6/6] feat: adopt UserModel to Encodable, Equatable and RawRepresentable --- .../Models/User/UserModel+Encodable.swift | 47 +++++++++++++++++++ Sources/VRCKit/Models/User/UserModel.swift | 20 ++++++++ 2 files changed, 67 insertions(+) create mode 100644 Sources/VRCKit/Models/User/UserModel+Encodable.swift diff --git a/Sources/VRCKit/Models/User/UserModel+Encodable.swift b/Sources/VRCKit/Models/User/UserModel+Encodable.swift new file mode 100644 index 0000000..5b0bd7d --- /dev/null +++ b/Sources/VRCKit/Models/User/UserModel+Encodable.swift @@ -0,0 +1,47 @@ +// +// UserModel+Encodable.swift +// VRCKit +// +// Created by makinosp on 2024/10/27. +// + +import Foundation + +extension User: Encodable { + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: UserCodingKeys.self) + try container.encode(activeFriends, forKey: .activeFriends) + try container.encode(allowAvatarCopying, forKey: .allowAvatarCopying) + try container.encodeIfPresent(bio, forKey: .bio) + try container.encode(bioLinks.wrappedValue, forKey: .bioLinks) + try container.encode(currentAvatar, forKey: .currentAvatar) + try container.encodeIfPresent(avatarImageUrl, forKey: .currentAvatarImageUrl) + try container.encodeIfPresent(avatarThumbnailUrl, forKey: .currentAvatarThumbnailImageUrl) + if let dateJoined = dateJoined { + let dateJoinedString = DateFormatter.dateStringFormat.string(from: dateJoined) + try container.encode(dateJoinedString, forKey: .dateJoined) + } + try container.encode(displayName, forKey: .displayName) + try container.encode(friendKey, forKey: .friendKey) + try container.encode(friends, forKey: .friends) + try container.encode(homeLocation, forKey: .homeLocation) + try container.encode(id, forKey: .id) + try container.encode(isFriend, forKey: .isFriend) + try container.encode(lastActivity, forKey: .lastActivity) + try container.encode(lastLogin, forKey: .lastLogin) + try container.encode(lastPlatform, forKey: .lastPlatform) + try container.encode(offlineFriends, forKey: .offlineFriends) + try container.encode(onlineFriends, forKey: .onlineFriends) + try container.encode(pastDisplayNames, forKey: .pastDisplayNames) + try container.encodeIfPresent(profilePicOverride, forKey: .profilePicOverride) + try container.encode(state, forKey: .state) + try container.encode(status, forKey: .status) + try container.encode(statusDescription, forKey: .statusDescription) + try container.encode(tags, forKey: .tags) + try container.encode(twoFactorAuthEnabled, forKey: .twoFactorAuthEnabled) + try container.encodeIfPresent(userIcon, forKey: .userIcon) + try container.encodeIfPresent(userLanguage, forKey: .userLanguage) + try container.encodeIfPresent(userLanguageCode, forKey: .userLanguageCode) + try container.encode(presence, forKey: .presence) + } +} diff --git a/Sources/VRCKit/Models/User/UserModel.swift b/Sources/VRCKit/Models/User/UserModel.swift index 64a10a1..d3760b7 100644 --- a/Sources/VRCKit/Models/User/UserModel.swift +++ b/Sources/VRCKit/Models/User/UserModel.swift @@ -51,6 +51,26 @@ public struct User: Sendable, ProfileDetailRepresentable { } } +extension User: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.hashValue == rhs.hashValue + } +} + +extension User: RawRepresentable { + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8) else { return nil } + guard let decoded: User = try? Serializer.shared.decode(data) else { return nil } + self = decoded + } + + public var rawValue: String { + guard let data = try? Serializer.shared.encode(self) else { return "" } + guard let encoded = String(data: data, encoding: .utf8) else { return "" } + return encoded + } +} + public extension User { var platform: UserPlatform { presence.platform } }