From d05d9fbfff2426c1d749139ba16e752e77fc05db Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Sun, 15 Jan 2023 14:51:09 +0100 Subject: [PATCH] Fix not posting status special characters or dropping part of it (Post in JSON now) --- Packages/Network/Sources/Network/Client.swift | 27 +++-- .../Sources/Network/Endpoint/Endpoint.swift | 7 ++ .../Sources/Network/Endpoint/Statuses.swift | 98 +++++++++---------- .../Status/Editor/StatusEditorViewModel.swift | 32 +++--- 4 files changed, 92 insertions(+), 72 deletions(-) diff --git a/Packages/Network/Sources/Network/Client.swift b/Packages/Network/Sources/Network/Client.swift index 13b17bbe..4812d488 100644 --- a/Packages/Network/Sources/Network/Client.swift +++ b/Packages/Network/Sources/Network/Client.swift @@ -66,18 +66,29 @@ public class Client: ObservableObject, Equatable { return components.url! } - private func makeURLRequest(url: URL, httpMethod: String) -> URLRequest { + private func makeURLRequest(url: URL, endpoint: Endpoint, httpMethod: String) -> URLRequest { var request = URLRequest(url: url) request.httpMethod = httpMethod if let oauthToken { request.setValue("Bearer \(oauthToken.accessToken)", forHTTPHeaderField: "Authorization") } + if let json = endpoint.jsonValue { + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + do { + let jsonData = try encoder.encode(json) + request.httpBody = jsonData + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + } catch { + print("Client Error encoding JSON: \(error.localizedDescription)") + } + } return request } private func makeGet(endpoint: Endpoint) -> URLRequest { let url = makeURL(endpoint: endpoint) - return makeURLRequest(url: url, httpMethod: "GET") + return makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET") } public func get(endpoint: Endpoint, forceVersion: Version? = nil) async throws -> Entity { @@ -101,14 +112,14 @@ public class Client: ObservableObject, Equatable { public func post(endpoint: Endpoint) async throws -> HTTPURLResponse? { let url = makeURL(endpoint: endpoint) - let request = makeURLRequest(url: url, httpMethod: "POST") + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "POST") let (_, httpResponse) = try await urlSession.data(for: request) return httpResponse as? HTTPURLResponse } public func patch(endpoint: Endpoint) async throws -> HTTPURLResponse? { let url = makeURL(endpoint: endpoint) - let request = makeURLRequest(url: url, httpMethod: "PATCH") + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "PATCH") let (_, httpResponse) = try await urlSession.data(for: request) return httpResponse as? HTTPURLResponse } @@ -119,7 +130,7 @@ public class Client: ObservableObject, Equatable { public func delete(endpoint: Endpoint) async throws -> HTTPURLResponse? { let url = makeURL(endpoint: endpoint) - let request = makeURLRequest(url: url, httpMethod: "DELETE") + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "DELETE") let (_, httpResponse) = try await urlSession.data(for: request) return httpResponse as? HTTPURLResponse } @@ -128,7 +139,7 @@ public class Client: ObservableObject, Equatable { method: String, forceVersion: Version? = nil) async throws -> Entity { let url = makeURL(endpoint: endpoint, forceVersion: forceVersion) - let request = makeURLRequest(url: url, httpMethod: method) + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) let (data, httpResponse) = try await urlSession.data(for: request) logResponseOnError(httpResponse: httpResponse, data: data) return try decoder.decode(Entity.self, from: data) @@ -157,7 +168,7 @@ public class Client: ObservableObject, Equatable { public func makeWebSocketTask(endpoint: Endpoint) -> URLSessionWebSocketTask { let url = makeURL(scheme: "wss", endpoint: endpoint) - let request = makeURLRequest(url: url, httpMethod: "GET") + let request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: "GET") return urlSession.webSocketTask(with: request) } @@ -168,7 +179,7 @@ public class Client: ObservableObject, Equatable { filename: String, data: Data) async throws -> Entity { let url = makeURL(endpoint: endpoint, forceVersion: version) - var request = makeURLRequest(url: url, httpMethod: method) + var request = makeURLRequest(url: url, endpoint: endpoint, httpMethod: method) let boundary = UUID().uuidString request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") let httpBody = NSMutableData() diff --git a/Packages/Network/Sources/Network/Endpoint/Endpoint.swift b/Packages/Network/Sources/Network/Endpoint/Endpoint.swift index c8a9ebd1..97df2fd1 100644 --- a/Packages/Network/Sources/Network/Endpoint/Endpoint.swift +++ b/Packages/Network/Sources/Network/Endpoint/Endpoint.swift @@ -3,6 +3,13 @@ import Foundation public protocol Endpoint { func path() -> String func queryItems() -> [URLQueryItem]? + var jsonValue: Encodable? { get } +} + +extension Endpoint { + public var jsonValue: Encodable? { + nil + } } extension Endpoint { diff --git a/Packages/Network/Sources/Network/Endpoint/Statuses.swift b/Packages/Network/Sources/Network/Endpoint/Statuses.swift index 200856ca..d6b0217f 100644 --- a/Packages/Network/Sources/Network/Endpoint/Statuses.swift +++ b/Packages/Network/Sources/Network/Endpoint/Statuses.swift @@ -2,19 +2,8 @@ import Foundation import Models public enum Statuses: Endpoint { - case postStatus(status: String, - inReplyTo: String?, - mediaIds: [String]?, - spoilerText: String?, - visibility: Visibility, - pollOptions: [String], - pollVotingFrequency: Bool?, - pollDuration: Int?) - case editStatus(id: String, - status: String, - mediaIds: [String]?, - spoilerText: String?, - visibility: Visibility) + case postStatus(json: StatusData) + case editStatus(id: String, json: StatusData) case status(id: String) case context(id: String) case favourite(id: String) @@ -34,7 +23,7 @@ public enum Statuses: Endpoint { return "statuses" case .status(let id): return "statuses/\(id)" - case .editStatus(let id, _, _, _, _): + case .editStatus(let id, _): return "statuses/\(id)" case .context(let id): return "statuses/\(id)/context" @@ -63,41 +52,6 @@ public enum Statuses: Endpoint { public func queryItems() -> [URLQueryItem]? { switch self { - case let .postStatus(status, inReplyTo, mediaIds, spoilerText, visibility, pollOptions, pollVotingFrequency, pollDuration): - var params: [URLQueryItem] = [.init(name: "status", value: status), - .init(name: "visibility", value: visibility.rawValue)] - if let inReplyTo { - params.append(.init(name: "in_reply_to_id", value: inReplyTo)) - } - if let mediaIds { - for mediaId in mediaIds { - params.append(.init(name: "media_ids[]", value: mediaId)) - } - } - if let spoilerText { - params.append(.init(name: "spoiler_text", value: spoilerText)) - } - if !pollOptions.isEmpty, let pollVotingFrequency, let pollDuration { - for option in pollOptions { - params.append(.init(name: "poll[options][]", value: option)) - } - - params.append(.init(name: "poll[multiple]", value: pollVotingFrequency ? "true" : "false")) - params.append(.init(name: "poll[expires_in]", value: "\(pollDuration)")) - } - return params - case let .editStatus(_, status, mediaIds, spoilerText, visibility): - var params: [URLQueryItem] = [.init(name: "status", value: status), - .init(name: "visibility", value: visibility.rawValue)] - if let mediaIds { - for mediaId in mediaIds { - params.append(.init(name: "media_ids[]", value: mediaId)) - } - } - if let spoilerText { - params.append(.init(name: "spoiler_text", value: spoilerText)) - } - return params case let .rebloggedBy(_, maxId): return makePaginationParam(sinceId: nil, maxId: maxId, mindId: nil) case let .favouritedBy(_, maxId): @@ -106,4 +60,50 @@ public enum Statuses: Endpoint { return nil } } + + public var jsonValue: Encodable? { + switch self { + case let .postStatus(json): + return json + case let .editStatus(_, json): + return json + default: + return nil + } + } +} + +public struct StatusData: Encodable { + public let status: String + public let visibility: Visibility + public let inReplyToId: String? + public let spoilerText: String? + public let mediaIds: [String]? + public let poll: PollData? + + public struct PollData: Encodable { + public let options: [String] + public let multiple: Bool + public let expires_in: Int + + public init(options: [String], multiple: Bool, expires_in: Int) { + self.options = options + self.multiple = multiple + self.expires_in = expires_in + } + } + + public init(status: String, + visibility: Visibility, + inReplyToId: String? = nil, + spoilerText: String? = nil, + mediaIds: [String]? = nil, + poll: PollData? = nil) { + self.status = status + self.visibility = visibility + self.inReplyToId = inReplyToId + self.spoilerText = spoilerText + self.mediaIds = mediaIds + self.poll = poll + } } diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index d90d24ba..b019af31 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -95,8 +95,9 @@ public class StatusEditorViewModel: ObservableObject { selectedRange = .init(location: text.utf16.count, length: 0) } - private func getPollOptionsForAPI() -> [String] { - pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } + private func getPollOptionsForAPI() -> [String]? { + let options = pollOptions.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } + return options.isEmpty ? nil : options } func postStatus() async -> Status? { @@ -104,22 +105,23 @@ public class StatusEditorViewModel: ObservableObject { do { isPosting = true let postStatus: Status? + var pollData: StatusData.PollData? + if let pollOptions = getPollOptionsForAPI() { + pollData = .init(options: pollOptions, + multiple: pollVotingFrequency.canVoteMultipleTimes, + expires_in: pollDuration.rawValue) + } + let data = StatusData(status: statusText.string, + visibility: visibility, + inReplyToId: mode.replyToStatus?.id, + spoilerText: spoilerOn ? spoilerText : nil, + mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id }, + poll: pollData) switch mode { case .new, .replyTo, .quote, .mention: - postStatus = try await client.post(endpoint: Statuses.postStatus(status: statusText.string, - inReplyTo: mode.replyToStatus?.id, - mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id }, - spoilerText: spoilerOn ? spoilerText : nil, - visibility: visibility, - pollOptions: getPollOptionsForAPI(), - pollVotingFrequency: pollVotingFrequency.canVoteMultipleTimes, - pollDuration: pollDuration.rawValue)) + postStatus = try await client.post(endpoint: Statuses.postStatus(json: data)) case let .edit(status): - postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, - status: statusText.string, - mediaIds: mediasImages.compactMap{ $0.mediaAttachement?.id }, - spoilerText: spoilerOn ? spoilerText : nil, - visibility: visibility)) + postStatus = try await client.put(endpoint: Statuses.editStatus(id: status.id, json: data)) } generator.notificationOccurred(.success) isPosting = false