From 6cc267689efa732eb19318f12b1c1b6b0878628c Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Thu, 29 Dec 2022 18:15:19 +0400 Subject: [PATCH] ios: fallback to manual parsing of apiChats and apiChat responses (#1659) --- .../Chat/ChatItem/CIInvalidJSONView.swift | 53 +++++++++++++++++++ apps/ios/Shared/Views/Chat/ChatItemView.swift | 1 + .../Views/ChatList/ChatListNavLink.swift | 14 +++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 4 ++ apps/ios/SimpleXChat/API.swift | 50 ++++++++++++++--- apps/ios/SimpleXChat/ChatTypes.swift | 52 ++++++++++++++++++ 6 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift new file mode 100644 index 000000000..0299a5e6f --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift @@ -0,0 +1,53 @@ +// +// CIInvalidJSONView.swift +// SimpleX (iOS) +// +// Created by JRoberts on 29.12.2022. +// Copyright © 2022 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct CIInvalidJSONView: View { + var json: String + @State private var showJSON = false + + var body: some View { + HStack(alignment: .bottom, spacing: 0) { + Text("invalid data") + .foregroundColor(.red) + .italic() + } + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color(uiColor: .tertiarySystemGroupedBackground)) + .cornerRadius(18) + .textSelection(.disabled) + .onTapGesture { showJSON = true } + .sheet(isPresented: $showJSON) { + invalidJSONView(json) + } + } +} + +func invalidJSONView(_ json: String) -> some View { + VStack(alignment: .leading, spacing: 16) { + Button { + showShareSheet(items: [json]) + } label: { + Image(systemName: "square.and.arrow.up") + } + .frame(maxWidth: .infinity, alignment: .trailing) + ScrollView { + Text(json) + } + } + .frame(maxHeight: .infinity) + .padding() +} + +struct CIInvalidJSONView_Previews: PreviewProvider { + static var previews: some View { + CIInvalidJSONView(json: "{}") + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 1d9c3a6a6..ccadb8db3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -72,6 +72,7 @@ struct ChatItemContentView: View { case let .sndGroupFeature(feature, preference, _): chatFeatureView(feature, preference.enable.iconColor) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red) + case let .invalidJSON(json): CIInvalidJSONView(json: json) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 9ab52ee71..81c642625 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -31,6 +31,7 @@ struct ChatListNavLink: View { @State private var showContactRequestDialog = false @State private var showJoinGroupDialog = false @State private var showContactConnectionInfo = false + @State private var showInvalidJSON = false var body: some View { switch chat.chatInfo { @@ -42,6 +43,8 @@ struct ChatListNavLink: View { contactRequestNavLink(cReq) case let .contactConnection(cConn): contactConnectionNavLink(cConn) + case let .invalidJSON(json): + invalidJSONPreview(json) } } @@ -335,6 +338,17 @@ struct ChatListNavLink: View { } } } + + private func invalidJSONPreview(_ json: String) -> some View { + Text("invalid chat data") + .foregroundColor(.red) + .padding(4) + .frame(height: rowHeights[dynamicTypeSize]) + .onTapGesture { showInvalidJSON = true } + .sheet(isPresented: $showInvalidJSON) { + invalidJSONView(json) + } + } } func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 90847878b..364f2f489 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ 5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; + 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; @@ -361,6 +362,7 @@ 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = ""; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; + 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; @@ -708,6 +710,7 @@ 644EFFE1292D089800525D5B /* FramedCIVoiceView.swift */, 644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */, 1841511920742C6E152E469F /* AnimatedImageView.swift */, + 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */, ); path = ChatItem; sourceTree = ""; @@ -1026,6 +1029,7 @@ 5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */, 5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */, 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */, + 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */, 644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */, 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 42ed1fbd3..cbb296e82 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -117,28 +117,64 @@ private func fromCString(_ c: UnsafeMutablePointer) -> String { public func chatResponse(_ s: String) -> ChatResponse { let d = s.data(using: .utf8)! -// TODO is there a way to do it without copying the data? e.g: -// let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) -// let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) + // TODO is there a way to do it without copying the data? e.g: + // let p = UnsafeMutableRawPointer.init(mutating: UnsafeRawPointer(cjson)) + // let d = Data.init(bytesNoCopy: p, count: strlen(cjson), deallocator: .free) do { let r = try jsonDecoder.decode(APIResponse.self, from: d) return r.resp } catch { logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") } - + var type: String? var json: String? if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary { - if let j1 = j["resp"] as? NSDictionary, j1.count == 1 { - type = j1.allKeys[0] as? String + if let jResp = j["resp"] as? NSDictionary, jResp.count == 1 { + type = jResp.allKeys[0] as? String + if type == "apiChats" { + if let jApiChats = jResp["apiChats"] as? NSDictionary, + let jChats = jApiChats["chats"] as? NSArray { + let chats = jChats.map { jChat in + if let chatData = try? parseChatData(jChat) { + return chatData + } + return ChatData.invalidJSON(prettyJSON(jChat) ?? "") + } + return .apiChats(chats: chats) + } + } else if type == "apiChat" { + if let jApiChat = jResp["apiChat"] as? NSDictionary, + let jChat = jApiChat["chat"] as? NSDictionary, + let chat = try? parseChatData(jChat) { + return .apiChat(chat: chat) + } + } } json = prettyJSON(j) } return ChatResponse.response(type: type ?? "invalid", json: json ?? s) } -func prettyJSON(_ obj: NSDictionary) -> String? { +func parseChatData(_ jChat: Any) throws -> ChatData { + let jChatDict = jChat as! NSDictionary + let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!) + let chatStats: ChatStats = try decodeObject(jChatDict["chatStats"]!) + let jChatItems = jChatDict["chatItems"] as! NSArray + let chatItems = jChatItems.map { jCI in + if let ci: ChatItem = try? decodeObject(jCI) { + return ci + } + return ChatItem.invalidJSON(prettyJSON(jCI) ?? "") + } + return ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats) +} + +func decodeObject(_ obj: Any) throws -> T { + try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj)) +} + +func prettyJSON(_ obj: Any) -> String? { if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) { return String(decoding: d, as: UTF8.self) } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 30b5f9097..f7f53005f 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -798,6 +798,9 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case group(groupInfo: GroupInfo) case contactRequest(contactRequest: UserContactRequest) case contactConnection(contactConnection: PendingContactConnection) + case invalidJSON(json: String) + + private static let invalidChatName = NSLocalizedString("invalid chat", comment: "invalid chat data") public var localDisplayName: String { get { @@ -806,6 +809,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.localDisplayName case let .contactRequest(contactRequest): return contactRequest.localDisplayName case let .contactConnection(contactConnection): return contactConnection.localDisplayName + case .invalidJSON: return ChatInfo.invalidChatName } } } @@ -817,6 +821,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.displayName case let .contactRequest(contactRequest): return contactRequest.displayName case let .contactConnection(contactConnection): return contactConnection.displayName + case .invalidJSON: return ChatInfo.invalidChatName } } } @@ -828,6 +833,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.fullName case let .contactRequest(contactRequest): return contactRequest.fullName case let .contactConnection(contactConnection): return contactConnection.fullName + case .invalidJSON: return ChatInfo.invalidChatName } } } @@ -839,6 +845,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.image case let .contactRequest(contactRequest): return contactRequest.image case let .contactConnection(contactConnection): return contactConnection.image + case .invalidJSON: return nil } } } @@ -850,6 +857,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.localAlias case let .contactRequest(contactRequest): return contactRequest.localAlias case let .contactConnection(contactConnection): return contactConnection.localAlias + case .invalidJSON: return "" } } } @@ -861,6 +869,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.id case let .contactRequest(contactRequest): return contactRequest.id case let .contactConnection(contactConnection): return contactConnection.id + case .invalidJSON: return "" } } } @@ -872,6 +881,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case .group: return .group case .contactRequest: return .contactRequest case .contactConnection: return .contactConnection + case .invalidJSON: return .direct } } } @@ -883,6 +893,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.apiId case let .contactRequest(contactRequest): return contactRequest.apiId case let .contactConnection(contactConnection): return contactConnection.apiId + case .invalidJSON: return 0 } } } @@ -894,6 +905,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.ready case let .contactRequest(contactRequest): return contactRequest.ready case let .contactConnection(contactConnection): return contactConnection.ready + case .invalidJSON: return false } } } @@ -905,6 +917,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.sendMsgEnabled case let .contactRequest(contactRequest): return contactRequest.sendMsgEnabled case let .contactConnection(contactConnection): return contactConnection.sendMsgEnabled + case .invalidJSON: return false } } } @@ -916,6 +929,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.membership.memberIncognito case .contactRequest: return false case let .contactConnection(contactConnection): return contactConnection.incognito + case .invalidJSON: return false } } } @@ -1003,6 +1017,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.createdAt case let .contactRequest(contactRequest): return contactRequest.createdAt case let .contactConnection(contactConnection): return contactConnection.createdAt + case .invalidJSON: return .now } } @@ -1012,6 +1027,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { case let .group(groupInfo): return groupInfo.updatedAt case let .contactRequest(contactRequest): return contactRequest.updatedAt case let .contactConnection(contactConnection): return contactConnection.updatedAt + case .invalidJSON: return .now } } @@ -1036,6 +1052,14 @@ public struct ChatData: Decodable, Identifiable { public var chatStats: ChatStats public var id: ChatId { get { chatInfo.id } } + + public static func invalidJSON(_ json: String) -> ChatData { + ChatData( + chatInfo: .invalidJSON(json: json), + chatItems: [], + chatStats: ChatStats() + ) + } } public struct ChatStats: Decodable { @@ -1714,6 +1738,7 @@ public struct ChatItem: Identifiable, Decodable { case .sndGroupFeature: return showNtfDir case .rcvChatFeatureRejected: return showNtfDir case .rcvGroupFeatureRejected: return showNtfDir + case .invalidJSON: return false } } @@ -1836,6 +1861,16 @@ public struct ChatItem: Identifiable, Decodable { file: nil ) } + + public static func invalidJSON(_ json: String) -> ChatItem { + ChatItem( + chatDir: CIDirection.directSnd, + meta: CIMeta.invalidJSON, + content: .invalidJSON(json: json), + quotedItem: nil, + file: nil + ) + } } public enum CIDirection: Decodable { @@ -1903,6 +1938,21 @@ public struct CIMeta: Decodable { editable: editable ) } + + public static var invalidJSON: CIMeta { + CIMeta( + itemId: 0, + itemTs: .now, + itemText: "invalid JSON", + itemStatus: .sndNew, + createdAt: .now, + updatedAt: .now, + itemDeleted: false, + itemEdited: false, + itemLive: false, + editable: false + ) + } } public struct CITimed: Decodable { @@ -1971,6 +2021,7 @@ public enum CIContent: Decodable, ItemContent { case sndGroupFeature(groupFeature: GroupFeature, preference: GroupPreference, param: Int?) case rcvChatFeatureRejected(feature: ChatFeature) case rcvGroupFeatureRejected(groupFeature: GroupFeature) + case invalidJSON(json: String) public var text: String { get { @@ -1996,6 +2047,7 @@ public enum CIContent: Decodable, ItemContent { case let .sndGroupFeature(feature, preference, param): return CIContent.featureText(feature, preference.enable.text, param) case let .rcvChatFeatureRejected(feature): return String.localizedStringWithFormat("%@: received, prohibited", feature.text) case let .rcvGroupFeatureRejected(groupFeature): return String.localizedStringWithFormat("%@: received, prohibited", groupFeature.text) + case .invalidJSON: return NSLocalizedString("invalid data", comment: "invalid chat item") } } }