From 091839c2e4203fdf8dc2cd54a95704ee20782eb6 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 8 Mar 2021 18:17:15 +0800 Subject: [PATCH 1/2] feat: add multipart helper. Add update credentials endpoint --- .../xcschemes/xcschememanagement.plist | 4 +- .../APIService/APIService+Account.swift | 33 +++ .../Mastodon+API+Account+Credentials.swift | 227 ++++++++++++++++++ .../API/Mastodon+API+Account.swift | 192 +-------------- .../MastodonSDK/API/Mastodon+API.swift | 9 +- .../Sources/MastodonSDK/Extension/Data.swift | 37 +++ .../Sources/MastodonSDK/Mastodon.swift | 1 + .../MediaAttachment.swift} | 20 +- .../Query/MultipartFormValue.swift | 37 +++ .../{Protocol => Query}/Query.swift | 20 +- .../API/MastodonSDK+API+AccountTests.swift | 17 +- 11 files changed, 391 insertions(+), 206 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift create mode 100644 MastodonSDK/Sources/MastodonSDK/Extension/Data.swift rename MastodonSDK/Sources/MastodonSDK/{Entity/Mastodon+Entity+MediaAttachment.swift => Query/MediaAttachment.swift} (66%) create mode 100644 MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift rename MastodonSDK/Sources/MastodonSDK/{Protocol => Query}/Query.swift (61%) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 60ccd3d87..6ec23cf5d 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 7 + 10 Mastodon - RTL.xcscheme_^#shared#^_ @@ -22,7 +22,7 @@ Mastodon.xcscheme_^#shared#^_ orderHint - 8 + 7 SuppressBuildableAutocreation diff --git a/Mastodon/Service/APIService/APIService+Account.swift b/Mastodon/Service/APIService/APIService+Account.swift index 6e26dbf83..2218bfa50 100644 --- a/Mastodon/Service/APIService/APIService+Account.swift +++ b/Mastodon/Service/APIService/APIService+Account.swift @@ -43,6 +43,39 @@ extension APIService { .eraseToAnyPublisher() } + func accountUpdateCredentials( + domain: String, + query: Mastodon.API.Account.UpdateCredentialQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + return Mastodon.API.Account.updateCredentials( + session: session, + domain: domain, + query: query, + authorization: authorization + ) + .flatMap { response -> AnyPublisher, Error> in + let log = OSLog.api + let account = response.value + + return self.backgroundManagedObjectContext.performChanges { + let (mastodonUser, isCreated) = APIService.CoreData.createOrMergeMastodonUser( + into: self.backgroundManagedObjectContext, + for: nil, + in: domain, + entity: account, + networkDate: response.networkDate, + log: log) + let flag = isCreated ? "+" : "-" + os_log(.info, log: log, "%{public}s[%{public}ld], %{public}s: mastodon user [%s](%s)%s verifed", ((#file as NSString).lastPathComponent), #line, #function, flag, mastodonUser.id, mastodonUser.username) + } + .setFailureType(to: Error.self) + .map { _ in return response } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + } + func accountRegister( domain: String, query: Mastodon.API.Account.RegisterQuery, diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift new file mode 100644 index 000000000..04273188b --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift @@ -0,0 +1,227 @@ +// +// Mastodon+API+Account+Credentials.swift +// +// +// Created by MainasuK Cirno on 2021-3-8. +// + +import Foundation +import Combine + +// MARK: - Account credentials +extension Mastodon.API.Account { + + static func accountsEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts") + } + + /// Register an account + /// + /// Creates a user and account records. + /// + /// - Since: 2.7.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/2/9 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `RegisterQuery` with account registration information + /// - authorization: App token + /// - Returns: `AnyPublisher` contains `Token` nested in the response + public static func register( + session: URLSession, + domain: String, + query: RegisterQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.post( + url: accountsEndpointURL(domain: domain), + query: query, + authorization: authorization + ) + 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() + } + + public struct RegisterQuery: Codable, PostQuery { + public let reason: String? + public let username: String + public let email: String + public let password: String + public let agreement: Bool + public let locale: String + + public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) { + self.reason = reason + self.username = username + self.email = email + self.password = password + self.agreement = agreement + self.locale = locale + } + } + +} + +extension Mastodon.API.Account { + + static func verifyCredentialsEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/verify_credentials") + } + + /// Verify account credentials + /// + /// Test to make sure that the user token works. + /// + /// - Since: 0.0.0 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/2/9 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - authorization: App token + /// - Returns: `AnyPublisher` contains `Account` nested in the response + 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() + } + + static func updateCredentialsEndpointURL(domain: String) -> URL { + return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/update_credentials") + } + + /// Update account credentials + /// + /// Update the user's display and preferences. + /// + /// - Since: 1.1.1 + /// - Version: 3.3.0 + /// # Last Update + /// 2021/2/9 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: `CredentialQuery` with update credential information + /// - authorization: user token + /// - Returns: `AnyPublisher` contains updated `Account` nested in the response + public static func updateCredentials( + session: URLSession, + domain: String, + query: UpdateCredentialQuery, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.patch( + url: updateCredentialsEndpointURL(domain: domain), + query: query, + 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() + } + + public struct UpdateCredentialQuery: PatchQuery { + public let discoverable: Bool? + public let bot: Bool? + public let displayName: String? + public let note: String? + public let avatar: Mastodon.Query.MediaAttachment? + public let header: Mastodon.Query.MediaAttachment? + public let locked: Bool? + public let source: Mastodon.Entity.Source? + public let fieldsAttributes: [Mastodon.Entity.Field]? + + enum CodingKeys: String, CodingKey { + case discoverable + case bot + case displayName = "display_name" + case note + + case avatar + case header + case locked + case source + case fieldsAttributes = "fields_attributes" + } + + public init( + discoverable: Bool? = nil, + bot: Bool? = nil, + displayName: String? = nil, + note: String? = nil, + avatar: Mastodon.Query.MediaAttachment? = nil, + header: Mastodon.Query.MediaAttachment? = nil, + locked: Bool? = nil, + source: Mastodon.Entity.Source? = nil, + fieldsAttributes: [Mastodon.Entity.Field]? = nil + ) { + self.discoverable = discoverable + self.bot = bot + self.displayName = displayName + self.note = note + self.avatar = avatar + self.header = header + self.locked = locked + self.source = source + self.fieldsAttributes = fieldsAttributes + } + + var contentType: String? { + return Self.multipartContentType() + } + + var body: Data? { + var data = Data() + + discoverable.flatMap { data.append(Data.multipart(key: "discoverable", value: $0)) } + bot.flatMap { data.append(Data.multipart(key: "bot", value: $0)) } + displayName.flatMap { data.append(Data.multipart(key: "display_name", value: $0)) } + note.flatMap { data.append(Data.multipart(key: "note", value: $0)) } + avatar.flatMap { data.append(Data.multipart(key: "avatar", value: $0)) } + header.flatMap { data.append(Data.multipart(key: "header", value: $0)) } + locked.flatMap { data.append(Data.multipart(key: "locked", value: $0)) } + if let source = source { + source.privacy.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0.rawValue)) } + source.sensitive.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } + source.language.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } + } + fieldsAttributes.flatMap { fieldsAttributes in + for fieldsAttribute in fieldsAttributes { + data.append(Data.multipart(key: "fields_attributes[name][]", value: fieldsAttribute.name)) + data.append(Data.multipart(key: "fields_attributes[value][]", value: fieldsAttribute.value)) + } + } + data.append(Data.multipartEnd()) + return data + } + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift index d9b2a4448..3bb81dc6b 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account.swift @@ -8,119 +8,17 @@ import Foundation import Combine +// MARK: - Retrieve information extension Mastodon.API.Account { - - static func verifyCredentialsEndpointURL(domain: String) -> URL { - return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/verify_credentials") - } - static func accountsEndpointURL(domain: String) -> URL { - return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts") - } + static func accountsInfoEndpointURL(domain: String, id: String) -> URL { - return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts") + return Mastodon.API.endpointURL(domain: domain) + .appendingPathComponent("accounts") .appendingPathComponent(id) } - static func updateCredentialsEndpointURL(domain: String) -> URL { - return Mastodon.API.endpointURL(domain: domain).appendingPathComponent("accounts/update_credentials") - } - /// Test to make sure that the user token works. + /// Retrieve information /// - /// - Since: 0.0.0 - /// - Version: 3.3.0 - /// # Last Update - /// 2021/2/9 - /// # Reference - /// [Document](https://docs.joinmastodon.org/methods/accounts/) - /// - Parameters: - /// - session: `URLSession` - /// - domain: Mastodon instance domain. e.g. "example.com" - /// - authorization: App token - /// - Returns: `AnyPublisher` contains `Account` nested in the response - 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() - } - - /// Creates a user and account records. - /// - /// - Since: 2.7.0 - /// - Version: 3.3.0 - /// # Last Update - /// 2021/2/9 - /// # Reference - /// [Document](https://docs.joinmastodon.org/methods/accounts/) - /// - Parameters: - /// - session: `URLSession` - /// - domain: Mastodon instance domain. e.g. "example.com" - /// - query: `RegisterQuery` with account registration information - /// - authorization: App token - /// - Returns: `AnyPublisher` contains `Token` nested in the response - public static func register( - session: URLSession, - domain: String, - query: RegisterQuery, - authorization: Mastodon.API.OAuth.Authorization - ) -> AnyPublisher, Error> { - let request = Mastodon.API.post( - url: accountsEndpointURL(domain: domain), - query: query, - authorization: authorization - ) - 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() - } - - /// Update the user's display and preferences. - /// - /// - Since: 1.1.1 - /// - Version: 3.3.0 - /// # Last Update - /// 2021/2/9 - /// # Reference - /// [Document](https://docs.joinmastodon.org/methods/accounts/) - /// - Parameters: - /// - session: `URLSession` - /// - domain: Mastodon instance domain. e.g. "example.com" - /// - query: `CredentialQuery` with update credential information - /// - authorization: user token - /// - Returns: `AnyPublisher` contains updated `Account` nested in the response - public static func updateCredentials( - session: URLSession, - domain: String, - query: UpdateCredentialQuery, - authorization: Mastodon.API.OAuth.Authorization - ) -> AnyPublisher, Error> { - let request = Mastodon.API.patch( - url: updateCredentialsEndpointURL(domain: domain), - query: query, - 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() - } - /// View information about a profile. /// /// - Since: 0.0.0 @@ -138,11 +36,11 @@ extension Mastodon.API.Account { public static func accountInfo( session: URLSession, domain: String, - query: AccountInfoQuery, + userID: Mastodon.Entity.Account.ID, authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { let request = Mastodon.API.get( - url: accountsInfoEndpointURL(domain: domain, id: query.id), + url: accountsInfoEndpointURL(domain: domain, id: userID), query: nil, authorization: authorization ) @@ -155,79 +53,3 @@ extension Mastodon.API.Account { } } - -extension Mastodon.API.Account { - - public struct RegisterQuery: Codable, PostQuery { - public let reason: String? - public let username: String - public let email: String - public let password: String - public let agreement: Bool - public let locale: String - - public init(reason: String? = nil, username: String, email: String, password: String, agreement: Bool, locale: String) { - self.reason = reason - self.username = username - self.email = email - self.password = password - self.agreement = agreement - self.locale = locale - } - } - - public struct UpdateCredentialQuery: Codable, PatchQuery { - - public var discoverable: Bool? - public var bot: Bool? - public var displayName: String? - public var note: String? - public var avatar: String? - public var header: String? - public var locked: Bool? - public var source: Mastodon.Entity.Source? - public var fieldsAttributes: [Mastodon.Entity.Field]? - - enum CodingKeys: String, CodingKey { - case discoverable - case bot - case displayName = "display_name" - case note - - case avatar - case header - case locked - case source - case fieldsAttributes = "fields_attributes" - } - - public init( - discoverable: Bool? = nil, - bot: Bool? = nil, - displayName: String? = nil, - note: String? = nil, - avatar: Mastodon.Entity.MediaAttachment? = nil, - header: Mastodon.Entity.MediaAttachment? = nil, - locked: Bool? = nil, - source: Mastodon.Entity.Source? = nil, - fieldsAttributes: [Mastodon.Entity.Field]? = nil - ) { - self.discoverable = discoverable - self.bot = bot - self.displayName = displayName - self.note = note - self.avatar = avatar?.base64EncondedString - self.header = header?.base64EncondedString - self.locked = locked - self.source = source - self.fieldsAttributes = fieldsAttributes - } - } - - public struct AccountInfoQuery: GetQuery { - - public let id: String - - var queryItems: [URLQueryItem]? { nil } - } -} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index 5a55ee103..073d926e9 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -140,8 +140,13 @@ extension Mastodon.API { timeoutInterval: Mastodon.API.timeoutInterval ) request.httpMethod = method.rawValue - request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") - request.httpBody = query?.body + if let contentType = query?.contentType { + request.setValue(contentType, forHTTPHeaderField: "Content-Type") + } + if let body = query?.body { + request.httpBody = body + request.setValue("\(body.count)", forHTTPHeaderField: "Content-Length") + } if let authorization = authorization { request.setValue( "Bearer \(authorization.accessToken)", diff --git a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift new file mode 100644 index 000000000..43354394d --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift @@ -0,0 +1,37 @@ +// +// Data.swift +// +// +// Created by MainasuK Cirno on 2021-3-8. +// + +import Foundation + +extension Data { + + static func multipart( + boundary: String = Multipart.boundary, + key: String, + value: MultipartFormValue + ) -> Data { + var data = Data() + data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!) + data.append("Content-Disposition: form-data; name=\"\(key)\"".data(using: .utf8)!) + if let filename = value.multipartFilename { + data.append("; filename=\"\(filename)\"\r\n".data(using: .utf8)!) + } else { + data.append("\r\n".data(using: .utf8)!) + } + if let contentType = value.multipartContentType { + data.append("Content-Type: \(contentType)\r\n".data(using: .utf8)!) + } + data.append("\r\n".data(using: .utf8)!) + data.append(value.multipartValue) + return data + } + + static func multipartEnd(boundary: String = Multipart.boundary) -> Data { + return "\r\n--\(boundary)--\r\n".data(using: .utf8)! + } + +} diff --git a/MastodonSDK/Sources/MastodonSDK/Mastodon.swift b/MastodonSDK/Sources/MastodonSDK/Mastodon.swift index 0c5e90609..b64d3726e 100644 --- a/MastodonSDK/Sources/MastodonSDK/Mastodon.swift +++ b/MastodonSDK/Sources/MastodonSDK/Mastodon.swift @@ -12,4 +12,5 @@ public enum Mastodon { public enum Response { } public enum API { } public enum Entity { } + public enum Query { } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+MediaAttachment.swift b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift similarity index 66% rename from MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+MediaAttachment.swift rename to MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift index 39ffe23d7..f3bd88832 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+MediaAttachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift @@ -1,5 +1,5 @@ // -// Mastodon+Entity+MediaAttachment.swift +// MediaAttachment.swift // // // Created by jk234ert on 2/9/21. @@ -7,7 +7,7 @@ import Foundation -extension Mastodon.Entity { +extension Mastodon.Query { public enum MediaAttachment { /// JPEG (Joint Photographic Experts Group) image case jpeg(Data?) @@ -20,7 +20,7 @@ extension Mastodon.Entity { } } -extension Mastodon.Entity.MediaAttachment { +extension Mastodon.Query.MediaAttachment { var data: Data? { switch self { case .jpeg(let data): return data @@ -31,11 +31,12 @@ extension Mastodon.Entity.MediaAttachment { } var fileName: String { + let name = UUID().uuidString switch self { - case .jpeg: return "file.jpg" - case .gif: return "file.gif" - case .png: return "file.png" - case .other(_, let fileExtension, _): return "file.\(fileExtension)" + case .jpeg: return "\(name).jpg" + case .gif: return "\(name).gif" + case .png: return "\(name).png" + case .other(_, let fileExtension, _): return "\(name).\(fileExtension)" } } @@ -53,3 +54,8 @@ extension Mastodon.Entity.MediaAttachment { } } +extension Mastodon.Query.MediaAttachment: MultipartFormValue { + var multipartValue: Data { return data ?? Data() } + var multipartContentType: String? { return mimeType } + var multipartFilename: String? { return fileName } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift new file mode 100644 index 000000000..fd9a9c8f4 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift @@ -0,0 +1,37 @@ +// +// MultipartFormValue.swift +// +// +// Created by MainasuK Cirno on 2021-3-8. +// + +import Foundation + +enum Multipart { + static let boundary = "__boundary__" +} + +protocol MultipartFormValue { + var multipartValue: Data { get } + var multipartContentType: String? { get } + var multipartFilename: String? { get } +} + +extension Bool: MultipartFormValue { + var multipartValue: Data { + switch self { + case true: return "true".data(using: .utf8)! + case false: return "false".data(using: .utf8)! + } + } + var multipartContentType: String? { return nil } + var multipartFilename: String? { return nil } +} + +extension String: MultipartFormValue { + var multipartValue: Data { + return self.data(using: .utf8)! + } + var multipartContentType: String? { return nil } + var multipartFilename: String? { return nil } +} diff --git a/MastodonSDK/Sources/MastodonSDK/Protocol/Query.swift b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift similarity index 61% rename from MastodonSDK/Sources/MastodonSDK/Protocol/Query.swift rename to MastodonSDK/Sources/MastodonSDK/Query/Query.swift index 7a6608d66..a0a5e4eae 100644 --- a/MastodonSDK/Sources/MastodonSDK/Protocol/Query.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/Query.swift @@ -14,12 +14,22 @@ enum RequestMethod: String { protocol RequestQuery { // All kinds of queries could have queryItems and body var queryItems: [URLQueryItem]? { get } + var contentType: String? { get } var body: Data? { get } } +extension RequestQuery { + static func multipartContentType(boundary: String = Multipart.boundary) -> String { + return "multipart/form-data; charset=utf-8; boundary=\"\(boundary)\"" + } +} + // An `Encodable` query provides its body by encoding itself // A `Get` query only contains queryItems, it should not be `Encodable` extension RequestQuery where Self: Encodable { + var contentType: String? { + return "application/json; charset=utf-8" + } var body: Data? { return try? Mastodon.API.encoder.encode(self) } @@ -30,18 +40,20 @@ protocol GetQuery: RequestQuery { } extension GetQuery { // By default a `GetQuery` does not has data body var body: Data? { nil } + var contentType: String? { nil } } -protocol PostQuery: RequestQuery & Encodable { } +protocol PostQuery: RequestQuery { } extension PostQuery { - // By default a `GetQuery` does not has query items + // By default a `PostQuery` does not has query items var queryItems: [URLQueryItem]? { nil } } -protocol PatchQuery: RequestQuery & Encodable { } +protocol PatchQuery: RequestQuery { } extension PatchQuery { - // By default a `GetQuery` does not has query items + // By default a `PatchQuery` does not has query items var queryItems: [URLQueryItem]? { nil } } + diff --git a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AccountTests.swift b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AccountTests.swift index 08607aed8..b113672ec 100644 --- a/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AccountTests.swift +++ b/MastodonSDK/Tests/MastodonSDKTests/API/MastodonSDK+API+AccountTests.swift @@ -8,9 +8,11 @@ import os.log import XCTest import Combine +import UIKit @testable import MastodonSDK extension MastodonSDKTests { + func testVerifyCredentials() throws { let theExpectation = expectation(description: "Verify Account Credentials") @@ -44,11 +46,14 @@ extension MastodonSDKTests { .flatMap({ (result) -> AnyPublisher, Error> in // TODO: replace with test account acct - XCTAssertEqual(result.value.acct, "") + XCTAssert(!result.value.acct.isEmpty) theExpectation1.fulfill() - - var query = Mastodon.API.Account.UpdateCredentialQuery() - query.note = dateString + + let query = Mastodon.API.Account.UpdateCredentialQuery( + bot: !(result.value.bot ?? false), + note: dateString, + header: Mastodon.Query.MediaAttachment.jpeg(UIImage(systemName: "house")!.jpegData(compressionQuality: 0.8)) + ) return Mastodon.API.Account.updateCredentials(session: self.session, domain: self.domain, query: query, authorization: authorization) }) .sink { completion in @@ -73,8 +78,7 @@ extension MastodonSDKTests { func testRetrieveAccountInfo() throws { let theExpectation = expectation(description: "Verify Account Credentials") - let query = Mastodon.API.Account.AccountInfoQuery(id: "1") - Mastodon.API.Account.accountInfo(session: session, domain: domain, query: query, authorization: nil) + Mastodon.API.Account.accountInfo(session: session, domain: "mastodon.online", userID: "1", authorization: nil) .receive(on: DispatchQueue.main) .sink { completion in switch completion { @@ -91,4 +95,5 @@ extension MastodonSDKTests { wait(for: [theExpectation], timeout: 5.0) } + } From 1a66ba92c0a38c68795b40ab7ed07a365b771fb9 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 8 Mar 2021 19:05:15 +0800 Subject: [PATCH 2/2] feat: add avatar and display name update logic after sign-up flow --- .../MastodonConfirmEmailViewController.swift | 19 ++++++++++++++- .../MastodonConfirmEmailViewModel.swift | 11 ++++++++- .../MastodonRegisterViewController.swift | 24 +++++++++++++++---- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift index 2d69f0dd3..dee1510cd 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewController.swift @@ -111,7 +111,24 @@ extension MastodonConfirmEmailViewController { 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) case .finished: - break + // upload avatar and set display name in the background + self.context.apiService.accountUpdateCredentials( + domain: self.viewModel.authenticateInfo.domain, + query: self.viewModel.updateCredentialQuery, + authorization: Mastodon.API.OAuth.Authorization(accessToken: self.viewModel.userToken.accessToken) + ) + .retry(3) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: setup avatar & display name fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: setup avatar & display name success", ((#file as NSString).lastPathComponent), #line, #function) + } + } receiveValue: { _ in + // do nothing + } + .store(in: &self.context.disposeBag) // execute in the background } } receiveValue: { response in os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user %s's email confirmed", ((#file as NSString).lastPathComponent), #line, #function, response.value.username) diff --git a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift index aff254741..9fbd24eda 100644 --- a/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift +++ b/Mastodon/Scene/Onboarding/ConfirmEmail/MastodonConfirmEmailViewModel.swift @@ -12,20 +12,29 @@ import MastodonSDK final class MastodonConfirmEmailViewModel { var disposeBag = Set() + // input let context: AppContext var email: String let authenticateInfo: AuthenticationViewModel.AuthenticateInfo let userToken: Mastodon.Entity.Token + let updateCredentialQuery: Mastodon.API.Account.UpdateCredentialQuery let timestampUpdatePublisher = Timer.publish(every: 4.0, on: .main, in: .common) .autoconnect() .share() .eraseToAnyPublisher() - init(context: AppContext, email: String, authenticateInfo: AuthenticationViewModel.AuthenticateInfo, userToken: Mastodon.Entity.Token) { + init( + context: AppContext, + email: String, + authenticateInfo: AuthenticationViewModel.AuthenticateInfo, + userToken: Mastodon.Entity.Token, + updateCredentialQuery: Mastodon.API.Account.UpdateCredentialQuery + ) { self.context = context self.email = email self.authenticateInfo = authenticateInfo self.userToken = userToken + self.updateCredentialQuery = updateCredentialQuery } } diff --git a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift index d1ef11c67..f078e9b8d 100644 --- a/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift +++ b/Mastodon/Scene/Onboarding/Register/MastodonRegisterViewController.swift @@ -5,12 +5,12 @@ // Created by MainasuK Cirno on 2021-2-5. // +import AlamofireImage import Combine import MastodonSDK import os.log import PhotosUI import UIKit -import UITextField_Shake final class MastodonRegisterViewController: UIViewController, NeedsDependency, OnboardingViewControllerAppearance { var disposeBag = Set() @@ -623,10 +623,10 @@ extension MastodonRegisterViewController { username: username, email: email, password: password, - agreement: true, // TODO: - locale: "en" // TODO: + agreement: true, // user confirmed in the server rules scene + locale: Locale.current.languageCode ?? "en" ) - + // register without show server rules context.apiService.accountRegister( domain: viewModel.domain, @@ -646,7 +646,21 @@ extension MastodonRegisterViewController { } receiveValue: { [weak self] response in guard let self = self else { return } let userToken = response.value - let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken) + let updateCredentialQuery: Mastodon.API.Account.UpdateCredentialQuery = { + let displayName: String? = self.viewModel.displayName.value.isEmpty ? nil : self.viewModel.displayName.value + let avatar: Mastodon.Query.MediaAttachment? = { + guard let avatarImage = self.viewModel.avatarImage.value else { return nil } + guard avatarImage.size.width <= 400 else { + return .jpeg(avatarImage.af.imageScaled(to: CGSize(width: 400, height: 400)).jpegData(compressionQuality: 0.8)) + } + return .jpeg(avatarImage.jpegData(compressionQuality: 0.8)) + }() + return Mastodon.API.Account.UpdateCredentialQuery( + displayName: displayName, + avatar: avatar + ) + }() + let viewModel = MastodonConfirmEmailViewModel(context: self.context, email: email, authenticateInfo: self.viewModel.authenticateInfo, userToken: userToken, updateCredentialQuery: updateCredentialQuery) self.coordinator.present(scene: .mastodonConfirmEmail(viewModel: viewModel), from: self, transition: .show) } .store(in: &disposeBag)