mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
core: pass event and response error without dedicated constructor (#5869)
* core: pass event and response error without dedicated constructor * ios: WIP * android, desktop: update UI for new API * ios: fix parser * fix showing invalid chats * fix mobile api tests * ios: split ChatResponse to 3 enums, decode API results on the same thread * tweak types * remove throws * rename
This commit is contained in:
parent
a0d1cca389
commit
24b0f0290b
54 changed files with 2131 additions and 2177 deletions
|
@ -54,7 +54,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
|
||||
m.tokenStatus = .active
|
||||
} catch {
|
||||
if let cr = error as? ChatResponse, case .chatCmdError(_, .errorAgent(.NTF(.AUTH))) = cr {
|
||||
if let cr = error as? ChatError, case .errorAgent(.NTF(.AUTH)) = cr {
|
||||
m.tokenStatus = .expired
|
||||
}
|
||||
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")
|
||||
|
|
|
@ -580,8 +580,8 @@ enum ChatCommand: ChatCmdProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
||||
case response(type: String, json: String)
|
||||
// ChatResponse is split to three enums to reduce stack size used when parsing it, parsing large enums is very inefficient.
|
||||
enum ChatResponse0: Decodable, ChatAPIResult {
|
||||
case activeUser(user: User)
|
||||
case usersList(users: [UserInfo])
|
||||
case chatStarted
|
||||
|
@ -611,6 +611,95 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
|
||||
case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
|
||||
case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64])
|
||||
|
||||
var responseType: String {
|
||||
switch self {
|
||||
case .activeUser: "activeUser"
|
||||
case .usersList: "usersList"
|
||||
case .chatStarted: "chatStarted"
|
||||
case .chatRunning: "chatRunning"
|
||||
case .chatStopped: "chatStopped"
|
||||
case .apiChats: "apiChats"
|
||||
case .apiChat: "apiChat"
|
||||
case .chatTags: "chatTags"
|
||||
case .chatItemInfo: "chatItemInfo"
|
||||
case .serverTestResult: "serverTestResult"
|
||||
case .serverOperatorConditions: "serverOperators"
|
||||
case .userServers: "userServers"
|
||||
case .userServersValidation: "userServersValidation"
|
||||
case .usageConditions: "usageConditions"
|
||||
case .chatItemTTL: "chatItemTTL"
|
||||
case .networkConfig: "networkConfig"
|
||||
case .contactInfo: "contactInfo"
|
||||
case .groupMemberInfo: "groupMemberInfo"
|
||||
case .queueInfo: "queueInfo"
|
||||
case .contactSwitchStarted: "contactSwitchStarted"
|
||||
case .groupMemberSwitchStarted: "groupMemberSwitchStarted"
|
||||
case .contactSwitchAborted: "contactSwitchAborted"
|
||||
case .groupMemberSwitchAborted: "groupMemberSwitchAborted"
|
||||
case .contactRatchetSyncStarted: "contactRatchetSyncStarted"
|
||||
case .groupMemberRatchetSyncStarted: "groupMemberRatchetSyncStarted"
|
||||
case .contactCode: "contactCode"
|
||||
case .groupMemberCode: "groupMemberCode"
|
||||
case .connectionVerified: "connectionVerified"
|
||||
case .tagsUpdated: "tagsUpdated"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
switch self {
|
||||
case let .activeUser(user): return String(describing: user)
|
||||
case let .usersList(users): return String(describing: users)
|
||||
case .chatStarted: return noDetails
|
||||
case .chatRunning: return noDetails
|
||||
case .chatStopped: return noDetails
|
||||
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
|
||||
case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))")
|
||||
case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))")
|
||||
case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))")
|
||||
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
|
||||
case let .serverOperatorConditions(conditions): return "conditions: \(String(describing: conditions))"
|
||||
case let .userServers(u, userServers): return withUser(u, "userServers: \(String(describing: userServers))")
|
||||
case let .userServersValidation(u, serverErrors): return withUser(u, "serverErrors: \(String(describing: serverErrors))")
|
||||
case let .usageConditions(usageConditions, _, acceptedConditions): return "usageConditions: \(String(describing: usageConditions))\nacceptedConditions: \(String(describing: acceptedConditions))"
|
||||
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
|
||||
case let .networkConfig(networkConfig): return String(describing: networkConfig)
|
||||
case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))")
|
||||
case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))")
|
||||
case let .queueInfo(u, rcvMsgInfo, queueInfo):
|
||||
let msgInfo = if let info = rcvMsgInfo { encodeJSON(info) } else { "none" }
|
||||
return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))")
|
||||
case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
||||
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
||||
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
||||
case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))")
|
||||
}
|
||||
}
|
||||
|
||||
static func fallbackResult(_ type: String, _ json: NSDictionary) -> ChatResponse0? {
|
||||
if type == "apiChats" {
|
||||
if let r = parseApiChats(json) {
|
||||
return .apiChats(user: r.user, chats: r.chats)
|
||||
}
|
||||
} else if type == "apiChat" {
|
||||
if let jApiChat = json["apiChat"] as? NSDictionary,
|
||||
let user: UserRef = try? decodeObject(jApiChat["user"] as Any),
|
||||
let jChat = jApiChat["chat"] as? NSDictionary,
|
||||
let (chat, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) {
|
||||
return .apiChat(user: user, chat: chat, navInfo: navInfo)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatResponse1: Decodable, ChatAPIResult {
|
||||
case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection)
|
||||
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
|
||||
case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef)
|
||||
|
@ -620,6 +709,8 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
|
||||
case contactAlreadyExists(user: UserRef, contact: Contact)
|
||||
case contactDeleted(user: UserRef, contact: Contact)
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
case groupDeletedUser(user: UserRef, groupInfo: GroupInfo)
|
||||
case chatCleared(user: UserRef, chatInfo: ChatInfo)
|
||||
case userProfileNoChange(user: User)
|
||||
case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary)
|
||||
|
@ -644,6 +735,95 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case reactionMembers(user: UserRef, memberReactions: [MemberReaction])
|
||||
case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool)
|
||||
case contactsList(user: UserRef, contacts: [Contact])
|
||||
|
||||
var responseType: String {
|
||||
switch self {
|
||||
case .invitation: "invitation"
|
||||
case .connectionIncognitoUpdated: "connectionIncognitoUpdated"
|
||||
case .connectionUserChanged: "connectionUserChanged"
|
||||
case .connectionPlan: "connectionPlan"
|
||||
case .sentConfirmation: "sentConfirmation"
|
||||
case .sentInvitation: "sentInvitation"
|
||||
case .sentInvitationToContact: "sentInvitationToContact"
|
||||
case .contactAlreadyExists: "contactAlreadyExists"
|
||||
case .contactDeleted: "contactDeleted"
|
||||
case .contactConnectionDeleted: "contactConnectionDeleted"
|
||||
case .groupDeletedUser: "groupDeletedUser"
|
||||
case .chatCleared: "chatCleared"
|
||||
case .userProfileNoChange: "userProfileNoChange"
|
||||
case .userProfileUpdated: "userProfileUpdated"
|
||||
case .userPrivacy: "userPrivacy"
|
||||
case .contactAliasUpdated: "contactAliasUpdated"
|
||||
case .groupAliasUpdated: "groupAliasUpdated"
|
||||
case .connectionAliasUpdated: "connectionAliasUpdated"
|
||||
case .contactPrefsUpdated: "contactPrefsUpdated"
|
||||
case .userContactLink: "userContactLink"
|
||||
case .userContactLinkUpdated: "userContactLinkUpdated"
|
||||
case .userContactLinkCreated: "userContactLinkCreated"
|
||||
case .userContactLinkDeleted: "userContactLinkDeleted"
|
||||
case .acceptingContactRequest: "acceptingContactRequest"
|
||||
case .contactRequestRejected: "contactRequestRejected"
|
||||
case .networkStatuses: "networkStatuses"
|
||||
case .newChatItems: "newChatItems"
|
||||
case .groupChatItemsDeleted: "groupChatItemsDeleted"
|
||||
case .forwardPlan: "forwardPlan"
|
||||
case .chatItemUpdated: "chatItemUpdated"
|
||||
case .chatItemNotChanged: "chatItemNotChanged"
|
||||
case .chatItemReaction: "chatItemReaction"
|
||||
case .reactionMembers: "reactionMembers"
|
||||
case .chatItemsDeleted: "chatItemsDeleted"
|
||||
case .contactsList: "contactsList"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
switch self {
|
||||
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
|
||||
case .userProfileNoChange: return noDetails
|
||||
case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile))
|
||||
case let .userPrivacy(u, updatedUser): return withUser(u, String(describing: updatedUser))
|
||||
case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact))
|
||||
case let .groupAliasUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||
case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))")
|
||||
case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails)
|
||||
case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails)
|
||||
case let .userContactLinkCreated(u, connLink): return withUser(u, String(describing: connLink))
|
||||
case .userContactLinkDeleted: return noDetails
|
||||
case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact))
|
||||
case .contactRequestRejected: return noDetails
|
||||
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
|
||||
case let .newChatItems(u, chatItems):
|
||||
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
||||
return withUser(u, itemsString)
|
||||
case let .groupChatItemsDeleted(u, gInfo, chatItemIDs, byUser, member_):
|
||||
return withUser(u, "chatItemIDs: \(String(describing: chatItemIDs))\ngroupInfo: \(String(describing: gInfo))\nbyUser: \(byUser)\nmember_: \(String(describing: member_))")
|
||||
case let .forwardPlan(u, chatItemIds, forwardConfirmation): return withUser(u, "items: \(chatItemIds) forwardConfirmation: \(String(describing: forwardConfirmation))")
|
||||
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))")
|
||||
case let .reactionMembers(u, reaction): return withUser(u, "memberReactions: \(String(describing: reaction))")
|
||||
case let .chatItemsDeleted(u, items, byUser):
|
||||
let itemsString = items.map { item in
|
||||
"deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n")
|
||||
return withUser(u, itemsString + "\nbyUser: \(byUser)")
|
||||
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts))
|
||||
case let .invitation(u, connLinkInvitation, connection): return withUser(u, "connLinkInvitation: \(connLinkInvitation)\nconnection: \(connection)")
|
||||
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||
case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))")
|
||||
case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))")
|
||||
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
|
||||
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatResponse2: Decodable, ChatAPIResult {
|
||||
// group responses
|
||||
case groupCreated(user: UserRef, groupInfo: GroupInfo)
|
||||
case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember)
|
||||
|
@ -651,7 +831,6 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], withMessages: Bool)
|
||||
case leftMemberUser(user: UserRef, groupInfo: GroupInfo)
|
||||
case groupMembers(user: UserRef, group: SimpleXChat.Group)
|
||||
case groupDeletedUser(user: UserRef, groupInfo: GroupInfo)
|
||||
case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole)
|
||||
case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool)
|
||||
case groupUpdated(user: UserRef, toGroup: GroupInfo)
|
||||
|
@ -678,7 +857,6 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
|
||||
case ntfConns(ntfConns: [NtfConn])
|
||||
case connNtfMessages(receivedMsgs: [NtfMsgInfo?])
|
||||
case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection)
|
||||
// remote desktop responses
|
||||
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
|
||||
case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String)
|
||||
|
@ -689,306 +867,100 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case agentSubsTotal(user: UserRef, subsTotal: SMPServerSubs, hasSession: Bool)
|
||||
case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary)
|
||||
case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs)
|
||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||
case archiveExported(archiveErrors: [ArchiveError])
|
||||
case archiveImported(archiveErrors: [ArchiveError])
|
||||
case appSettings(appSettings: AppSettings)
|
||||
|
||||
var responseType: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .response(type, _): return "* \(type)"
|
||||
case .activeUser: return "activeUser"
|
||||
case .usersList: return "usersList"
|
||||
case .chatStarted: return "chatStarted"
|
||||
case .chatRunning: return "chatRunning"
|
||||
case .chatStopped: return "chatStopped"
|
||||
case .apiChats: return "apiChats"
|
||||
case .apiChat: return "apiChat"
|
||||
case .chatTags: return "chatTags"
|
||||
case .chatItemInfo: return "chatItemInfo"
|
||||
case .serverTestResult: return "serverTestResult"
|
||||
case .serverOperatorConditions: return "serverOperators"
|
||||
case .userServers: return "userServers"
|
||||
case .userServersValidation: return "userServersValidation"
|
||||
case .usageConditions: return "usageConditions"
|
||||
case .chatItemTTL: return "chatItemTTL"
|
||||
case .networkConfig: return "networkConfig"
|
||||
case .contactInfo: return "contactInfo"
|
||||
case .groupMemberInfo: return "groupMemberInfo"
|
||||
case .queueInfo: return "queueInfo"
|
||||
case .contactSwitchStarted: return "contactSwitchStarted"
|
||||
case .groupMemberSwitchStarted: return "groupMemberSwitchStarted"
|
||||
case .contactSwitchAborted: return "contactSwitchAborted"
|
||||
case .groupMemberSwitchAborted: return "groupMemberSwitchAborted"
|
||||
case .contactRatchetSyncStarted: return "contactRatchetSyncStarted"
|
||||
case .groupMemberRatchetSyncStarted: return "groupMemberRatchetSyncStarted"
|
||||
case .contactCode: return "contactCode"
|
||||
case .groupMemberCode: return "groupMemberCode"
|
||||
case .connectionVerified: return "connectionVerified"
|
||||
case .tagsUpdated: return "tagsUpdated"
|
||||
case .invitation: return "invitation"
|
||||
case .connectionIncognitoUpdated: return "connectionIncognitoUpdated"
|
||||
case .connectionUserChanged: return "connectionUserChanged"
|
||||
case .connectionPlan: return "connectionPlan"
|
||||
case .sentConfirmation: return "sentConfirmation"
|
||||
case .sentInvitation: return "sentInvitation"
|
||||
case .sentInvitationToContact: return "sentInvitationToContact"
|
||||
case .contactAlreadyExists: return "contactAlreadyExists"
|
||||
case .contactDeleted: return "contactDeleted"
|
||||
case .chatCleared: return "chatCleared"
|
||||
case .userProfileNoChange: return "userProfileNoChange"
|
||||
case .userProfileUpdated: return "userProfileUpdated"
|
||||
case .userPrivacy: return "userPrivacy"
|
||||
case .contactAliasUpdated: return "contactAliasUpdated"
|
||||
case .groupAliasUpdated: return "groupAliasUpdated"
|
||||
case .connectionAliasUpdated: return "connectionAliasUpdated"
|
||||
case .contactPrefsUpdated: return "contactPrefsUpdated"
|
||||
case .userContactLink: return "userContactLink"
|
||||
case .userContactLinkUpdated: return "userContactLinkUpdated"
|
||||
case .userContactLinkCreated: return "userContactLinkCreated"
|
||||
case .userContactLinkDeleted: return "userContactLinkDeleted"
|
||||
case .acceptingContactRequest: return "acceptingContactRequest"
|
||||
case .contactRequestRejected: return "contactRequestRejected"
|
||||
case .networkStatuses: return "networkStatuses"
|
||||
case .newChatItems: return "newChatItems"
|
||||
case .groupChatItemsDeleted: return "groupChatItemsDeleted"
|
||||
case .forwardPlan: return "forwardPlan"
|
||||
case .chatItemUpdated: return "chatItemUpdated"
|
||||
case .chatItemNotChanged: return "chatItemNotChanged"
|
||||
case .chatItemReaction: return "chatItemReaction"
|
||||
case .reactionMembers: return "reactionMembers"
|
||||
case .chatItemsDeleted: return "chatItemsDeleted"
|
||||
case .contactsList: return "contactsList"
|
||||
case .groupCreated: return "groupCreated"
|
||||
case .sentGroupInvitation: return "sentGroupInvitation"
|
||||
case .userAcceptedGroupSent: return "userAcceptedGroupSent"
|
||||
case .userDeletedMembers: return "userDeletedMembers"
|
||||
case .leftMemberUser: return "leftMemberUser"
|
||||
case .groupMembers: return "groupMembers"
|
||||
case .groupDeletedUser: return "groupDeletedUser"
|
||||
case .membersRoleUser: return "membersRoleUser"
|
||||
case .membersBlockedForAllUser: return "membersBlockedForAllUser"
|
||||
case .groupUpdated: return "groupUpdated"
|
||||
case .groupLinkCreated: return "groupLinkCreated"
|
||||
case .groupLink: return "groupLink"
|
||||
case .groupLinkDeleted: return "groupLinkDeleted"
|
||||
case .newMemberContact: return "newMemberContact"
|
||||
case .newMemberContactSentInv: return "newMemberContactSentInv"
|
||||
case .rcvFileAccepted: return "rcvFileAccepted"
|
||||
case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled"
|
||||
case .standaloneFileInfo: return "standaloneFileInfo"
|
||||
case .rcvStandaloneFileCreated: return "rcvStandaloneFileCreated"
|
||||
case .rcvFileCancelled: return "rcvFileCancelled"
|
||||
case .sndFileCancelled: return "sndFileCancelled"
|
||||
case .sndStandaloneFileCreated: return "sndStandaloneFileCreated"
|
||||
case .sndFileStartXFTP: return "sndFileStartXFTP"
|
||||
case .sndFileCancelledXFTP: return "sndFileCancelledXFTP"
|
||||
case .callInvitations: return "callInvitations"
|
||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||
case .ntfToken: return "ntfToken"
|
||||
case .ntfConns: return "ntfConns"
|
||||
case .connNtfMessages: return "connNtfMessages"
|
||||
case .contactConnectionDeleted: return "contactConnectionDeleted"
|
||||
case .remoteCtrlList: return "remoteCtrlList"
|
||||
case .remoteCtrlConnecting: return "remoteCtrlConnecting"
|
||||
case .remoteCtrlConnected: return "remoteCtrlConnected"
|
||||
case .versionInfo: return "versionInfo"
|
||||
case .cmdOk: return "cmdOk"
|
||||
case .agentSubsTotal: return "agentSubsTotal"
|
||||
case .agentServersSummary: return "agentServersSummary"
|
||||
case .agentSubsSummary: return "agentSubsSummary"
|
||||
case .chatCmdError: return "chatCmdError"
|
||||
case .archiveExported: return "archiveExported"
|
||||
case .archiveImported: return "archiveImported"
|
||||
case .appSettings: return "appSettings"
|
||||
}
|
||||
switch self {
|
||||
case .groupCreated: "groupCreated"
|
||||
case .sentGroupInvitation: "sentGroupInvitation"
|
||||
case .userAcceptedGroupSent: "userAcceptedGroupSent"
|
||||
case .userDeletedMembers: "userDeletedMembers"
|
||||
case .leftMemberUser: "leftMemberUser"
|
||||
case .groupMembers: "groupMembers"
|
||||
case .membersRoleUser: "membersRoleUser"
|
||||
case .membersBlockedForAllUser: "membersBlockedForAllUser"
|
||||
case .groupUpdated: "groupUpdated"
|
||||
case .groupLinkCreated: "groupLinkCreated"
|
||||
case .groupLink: "groupLink"
|
||||
case .groupLinkDeleted: "groupLinkDeleted"
|
||||
case .newMemberContact: "newMemberContact"
|
||||
case .newMemberContactSentInv: "newMemberContactSentInv"
|
||||
case .rcvFileAccepted: "rcvFileAccepted"
|
||||
case .rcvFileAcceptedSndCancelled: "rcvFileAcceptedSndCancelled"
|
||||
case .standaloneFileInfo: "standaloneFileInfo"
|
||||
case .rcvStandaloneFileCreated: "rcvStandaloneFileCreated"
|
||||
case .rcvFileCancelled: "rcvFileCancelled"
|
||||
case .sndFileCancelled: "sndFileCancelled"
|
||||
case .sndStandaloneFileCreated: "sndStandaloneFileCreated"
|
||||
case .sndFileStartXFTP: "sndFileStartXFTP"
|
||||
case .sndFileCancelledXFTP: "sndFileCancelledXFTP"
|
||||
case .callInvitations: "callInvitations"
|
||||
case .ntfTokenStatus: "ntfTokenStatus"
|
||||
case .ntfToken: "ntfToken"
|
||||
case .ntfConns: "ntfConns"
|
||||
case .connNtfMessages: "connNtfMessages"
|
||||
case .remoteCtrlList: "remoteCtrlList"
|
||||
case .remoteCtrlConnecting: "remoteCtrlConnecting"
|
||||
case .remoteCtrlConnected: "remoteCtrlConnected"
|
||||
case .versionInfo: "versionInfo"
|
||||
case .cmdOk: "cmdOk"
|
||||
case .agentSubsTotal: "agentSubsTotal"
|
||||
case .agentServersSummary: "agentServersSummary"
|
||||
case .agentSubsSummary: "agentSubsSummary"
|
||||
case .archiveExported: "archiveExported"
|
||||
case .archiveImported: "archiveImported"
|
||||
case .appSettings: "appSettings"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
get {
|
||||
switch self {
|
||||
case let .response(_, json): return json
|
||||
case let .activeUser(user): return String(describing: user)
|
||||
case let .usersList(users): return String(describing: users)
|
||||
case .chatStarted: return noDetails
|
||||
case .chatRunning: return noDetails
|
||||
case .chatStopped: return noDetails
|
||||
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
|
||||
case let .apiChat(u, chat, navInfo): return withUser(u, "chat: \(String(describing: chat))\nnavInfo: \(String(describing: navInfo))")
|
||||
case let .chatTags(u, userTags): return withUser(u, "userTags: \(String(describing: userTags))")
|
||||
case let .chatItemInfo(u, chatItem, chatItemInfo): return withUser(u, "chatItem: \(String(describing: chatItem))\nchatItemInfo: \(String(describing: chatItemInfo))")
|
||||
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\nresult: \(String(describing: testFailure))")
|
||||
case let .serverOperatorConditions(conditions): return "conditions: \(String(describing: conditions))"
|
||||
case let .userServers(u, userServers): return withUser(u, "userServers: \(String(describing: userServers))")
|
||||
case let .userServersValidation(u, serverErrors): return withUser(u, "serverErrors: \(String(describing: serverErrors))")
|
||||
case let .usageConditions(usageConditions, _, acceptedConditions): return "usageConditions: \(String(describing: usageConditions))\nacceptedConditions: \(String(describing: acceptedConditions))"
|
||||
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
|
||||
case let .networkConfig(networkConfig): return String(describing: networkConfig)
|
||||
case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))")
|
||||
case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))")
|
||||
case let .queueInfo(u, rcvMsgInfo, queueInfo):
|
||||
let msgInfo = if let info = rcvMsgInfo { encodeJSON(info) } else { "none" }
|
||||
return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))")
|
||||
case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberSwitchAborted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactRatchetSyncStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .groupMemberRatchetSyncStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))")
|
||||
case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)")
|
||||
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
|
||||
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
|
||||
case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))")
|
||||
case let .invitation(u, connLinkInvitation, connection): return withUser(u, "connLinkInvitation: \(connLinkInvitation)\nconnection: \(connection)")
|
||||
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||
case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))")
|
||||
case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))")
|
||||
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
|
||||
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .contactDeleted(u, contact): return withUser(u, String(describing: contact))
|
||||
case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
|
||||
case .userProfileNoChange: return noDetails
|
||||
case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile))
|
||||
case let .userPrivacy(u, updatedUser): return withUser(u, String(describing: updatedUser))
|
||||
case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact))
|
||||
case let .groupAliasUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
|
||||
case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))")
|
||||
case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails)
|
||||
case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails)
|
||||
case let .userContactLinkCreated(u, connLink): return withUser(u, String(describing: connLink))
|
||||
case .userContactLinkDeleted: return noDetails
|
||||
case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact))
|
||||
case .contactRequestRejected: return noDetails
|
||||
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
|
||||
case let .newChatItems(u, chatItems):
|
||||
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
||||
return withUser(u, itemsString)
|
||||
case let .groupChatItemsDeleted(u, gInfo, chatItemIDs, byUser, member_):
|
||||
return withUser(u, "chatItemIDs: \(String(describing: chatItemIDs))\ngroupInfo: \(String(describing: gInfo))\nbyUser: \(byUser)\nmember_: \(String(describing: member_))")
|
||||
case let .forwardPlan(u, chatItemIds, forwardConfirmation): return withUser(u, "items: \(chatItemIds) forwardConfirmation: \(String(describing: forwardConfirmation))")
|
||||
case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))")
|
||||
case let .reactionMembers(u, reaction): return withUser(u, "memberReactions: \(String(describing: reaction))")
|
||||
case let .chatItemsDeleted(u, items, byUser):
|
||||
let itemsString = items.map { item in
|
||||
"deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n")
|
||||
return withUser(u, itemsString + "\nbyUser: \(byUser)")
|
||||
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts))
|
||||
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)")
|
||||
case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))")
|
||||
case let .userDeletedMembers(u, groupInfo, members, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\nwithMessages: \(withMessages)")
|
||||
case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .groupMembers(u, group): return withUser(u, String(describing: group))
|
||||
case let .groupDeletedUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
|
||||
case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)")
|
||||
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
case let .groupLinkCreated(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
|
||||
case let .groupLink(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
|
||||
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case .rcvFileAcceptedSndCancelled: return noDetails
|
||||
case let .standaloneFileInfo(fileMeta): return String(describing: fileMeta)
|
||||
case .rcvStandaloneFileCreated: return noDetails
|
||||
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
||||
case .sndStandaloneFileCreated: return noDetails
|
||||
case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .callInvitations(invs): return String(describing: invs)
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
||||
case let .ntfConns(ntfConns): return String(describing: ntfConns)
|
||||
case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))"
|
||||
case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection))
|
||||
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||
case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)"
|
||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
||||
case .cmdOk: return noDetails
|
||||
case let .agentSubsTotal(u, subsTotal, hasSession): return withUser(u, "subsTotal: \(String(describing: subsTotal))\nhasSession: \(hasSession)")
|
||||
case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary))
|
||||
case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary))
|
||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
case let .archiveExported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .appSettings(appSettings): return String(describing: appSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var noDetails: String { get { "\(responseType): no details" } }
|
||||
|
||||
static 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)
|
||||
do {
|
||||
let r = try callWithLargeStack {
|
||||
try jsonDecoder.decode(APIResponse<ChatResponse>.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 jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if jResp.count == 2 && type == "_owsf" {
|
||||
type = jResp.allKeys[1] as? String
|
||||
}
|
||||
if type == "apiChats" {
|
||||
if let r = parseApiChats(jResp) {
|
||||
return .apiChats(user: r.user, chats: r.chats)
|
||||
}
|
||||
} else if type == "apiChat" {
|
||||
if let jApiChat = jResp["apiChat"] as? NSDictionary,
|
||||
let user: UserRef = try? decodeObject(jApiChat["user"] as Any),
|
||||
let jChat = jApiChat["chat"] as? NSDictionary,
|
||||
let (chat, navInfo) = try? parseChatData(jChat, jApiChat["navInfo"] as? NSDictionary) {
|
||||
return .apiChat(user: user, chat: chat, navInfo: navInfo)
|
||||
}
|
||||
} else if type == "chatCmdError" {
|
||||
if let jError = jResp["chatCmdError"] as? NSDictionary {
|
||||
return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
json = serializeJSON(j, options: .prettyPrinted)
|
||||
}
|
||||
return ChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
var chatError: ChatError? {
|
||||
switch self {
|
||||
case let .chatCmdError(_, error): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
var chatErrorType: ChatErrorType? {
|
||||
switch self {
|
||||
case let .chatCmdError(_, .error(error)): error
|
||||
default: nil
|
||||
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)")
|
||||
case let .userAcceptedGroupSent(u, groupInfo, hostContact): return withUser(u, "groupInfo: \(groupInfo)\nhostContact: \(String(describing: hostContact))")
|
||||
case let .userDeletedMembers(u, groupInfo, members, withMessages): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\nwithMessages: \(withMessages)")
|
||||
case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .groupMembers(u, group): return withUser(u, String(describing: group))
|
||||
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
|
||||
case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)")
|
||||
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
|
||||
case let .groupLinkCreated(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
|
||||
case let .groupLink(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
|
||||
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
|
||||
case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
|
||||
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
|
||||
case .rcvFileAcceptedSndCancelled: return noDetails
|
||||
case let .standaloneFileInfo(fileMeta): return String(describing: fileMeta)
|
||||
case .rcvStandaloneFileCreated: return noDetails
|
||||
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
|
||||
case .sndStandaloneFileCreated: return noDetails
|
||||
case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .callInvitations(invs): return String(describing: invs)
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)"
|
||||
case let .ntfConns(ntfConns): return String(describing: ntfConns)
|
||||
case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))"
|
||||
case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls)
|
||||
case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)"
|
||||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||
case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))"
|
||||
case .cmdOk: return noDetails
|
||||
case let .agentSubsTotal(u, subsTotal, hasSession): return withUser(u, "subsTotal: \(String(describing: subsTotal))\nhasSession: \(hasSession)")
|
||||
case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary))
|
||||
case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary))
|
||||
case let .archiveExported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
|
||||
case let .appSettings(appSettings): return String(describing: appSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatEvent: Decodable, ChatEventProtocol {
|
||||
case event(type: String, json: String)
|
||||
enum ChatEvent: Decodable, ChatAPIResult {
|
||||
case chatSuspended
|
||||
case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress)
|
||||
case groupMemberSwitch(user: UserRef, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress)
|
||||
|
@ -1063,11 +1035,9 @@ enum ChatEvent: Decodable, ChatEventProtocol {
|
|||
case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
|
||||
// pq
|
||||
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
|
||||
case chatError(user_: UserRef?, chatError: ChatError)
|
||||
|
||||
var eventType: String {
|
||||
var responseType: String {
|
||||
switch self {
|
||||
case let .event(type, _): "* \(type)"
|
||||
case .chatSuspended: "chatSuspended"
|
||||
case .contactSwitch: "contactSwitch"
|
||||
case .groupMemberSwitch: "groupMemberSwitch"
|
||||
|
@ -1135,13 +1105,11 @@ enum ChatEvent: Decodable, ChatEventProtocol {
|
|||
case .remoteCtrlConnected: "remoteCtrlConnected"
|
||||
case .remoteCtrlStopped: "remoteCtrlStopped"
|
||||
case .contactPQEnabled: "contactPQEnabled"
|
||||
case .chatError: "chatError"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
switch self {
|
||||
case let .event(_, json): return json
|
||||
case .chatSuspended: return noDetails
|
||||
case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))")
|
||||
case let .groupMemberSwitch(u, groupInfo, member, switchProgress): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nswitchProgress: \(String(describing: switchProgress))")
|
||||
|
@ -1217,84 +1185,8 @@ enum ChatEvent: Decodable, ChatEventProtocol {
|
|||
case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
|
||||
case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))"
|
||||
case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)")
|
||||
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
}
|
||||
}
|
||||
|
||||
private var noDetails: String { "\(eventType): no details" }
|
||||
|
||||
static func chatEvent(_ s: String) -> ChatEvent {
|
||||
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)
|
||||
do {
|
||||
let r = // try callWithLargeStack {
|
||||
try jsonDecoder.decode(APIResponse<ChatEvent>.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 jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if jResp.count == 2 && type == "_owsf" {
|
||||
type = jResp.allKeys[1] as? String
|
||||
}
|
||||
if type == "chatError" {
|
||||
if let jError = jResp["chatError"] as? NSDictionary {
|
||||
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
json = serializeJSON(j, options: .prettyPrinted)
|
||||
}
|
||||
return ChatEvent.event(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
var chatError: ChatError? {
|
||||
switch self {
|
||||
case let .chatError(_, error): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
var chatErrorType: ChatErrorType? {
|
||||
switch self {
|
||||
case let .chatError(_, .error(error)): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let largeStackSize: Int = 2 * 1024 * 1024
|
||||
|
||||
private func callWithLargeStack<T>(_ f: @escaping () throws -> T) throws -> T {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var result: Result<T, Error>?
|
||||
let thread = Thread {
|
||||
do {
|
||||
result = .success(try f())
|
||||
} catch {
|
||||
result = .failure(error)
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
thread.stackSize = largeStackSize
|
||||
thread.qualityOfService = Thread.current.qualityOfService
|
||||
thread.start()
|
||||
|
||||
semaphore.wait()
|
||||
|
||||
switch result! {
|
||||
case let .success(r): return r
|
||||
case let .failure(e): throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NewUser: Encodable {
|
||||
|
|
|
@ -30,9 +30,18 @@ actor TerminalItems {
|
|||
}
|
||||
}
|
||||
|
||||
func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async {
|
||||
func addCommand<R: ChatAPIResult>(_ start: Date, _ cmd: ChatCommand, _ res: APIResult<R>) async {
|
||||
await add(.cmd(start, cmd))
|
||||
await add(.resp(.now, resp))
|
||||
await addResult(res)
|
||||
}
|
||||
|
||||
func addResult<R: ChatAPIResult>(_ res: APIResult<R>) async {
|
||||
let item: TerminalItem = switch res {
|
||||
case let .result(r): .res(.now, r)
|
||||
case let .error(e): .err(.now, e)
|
||||
case let .invalid(type, json): .bad(.now, type, json)
|
||||
}
|
||||
await add(item)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -243,7 +243,7 @@ struct ActiveCallView: View {
|
|||
ChatReceiver.shared.messagesChannel = nil
|
||||
return
|
||||
}
|
||||
if case let .chatItemsStatusesUpdated(_, chatItems) = msg,
|
||||
if case let .result(.chatItemsStatusesUpdated(_, chatItems)) = msg,
|
||||
chatItems.contains(where: { ci in
|
||||
ci.chatInfo.id == call.contact.id &&
|
||||
ci.chatItem.content.isSndCall &&
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct CIInvalidJSONView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var json: String
|
||||
var json: Data?
|
||||
@State private var showJSON = false
|
||||
|
||||
var body: some View {
|
||||
|
@ -25,7 +26,7 @@ struct CIInvalidJSONView: View {
|
|||
.textSelection(.disabled)
|
||||
.onTapGesture { showJSON = true }
|
||||
.appSheet(isPresented: $showJSON) {
|
||||
invalidJSONView(json)
|
||||
invalidJSONView(dataToString(json))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +50,6 @@ func invalidJSONView(_ json: String) -> some View {
|
|||
|
||||
struct CIInvalidJSONView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CIInvalidJSONView(json: "{}")
|
||||
CIInvalidJSONView(json: "{}".data(using: .utf8)!)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -579,14 +579,14 @@ struct ChatListNavLink: View {
|
|||
)
|
||||
}
|
||||
|
||||
private func invalidJSONPreview(_ json: String) -> some View {
|
||||
private func invalidJSONPreview(_ json: Data?) -> some View {
|
||||
Text("invalid chat data")
|
||||
.foregroundColor(.red)
|
||||
.padding(4)
|
||||
.frame(height: dynamicRowHeight)
|
||||
.onTapGesture { showInvalidJSON = true }
|
||||
.appSheet(isPresented: $showInvalidJSON) {
|
||||
invalidJSONView(json)
|
||||
invalidJSONView(dataToString(json))
|
||||
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil)
|
||||
}
|
||||
}
|
||||
|
@ -689,7 +689,7 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) {
|
|||
}
|
||||
|
||||
func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
|
||||
if let r = error as? ChatResponse,
|
||||
if let r = error as? ChatError,
|
||||
let alert = getNetworkErrorAlert(r) {
|
||||
return alert
|
||||
} else {
|
||||
|
|
|
@ -173,7 +173,7 @@ struct DatabaseEncryptionView: View {
|
|||
}
|
||||
return true
|
||||
} catch let error {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
if case .errorDatabase(.errorExport(.errorNotADatabase)) = error as? ChatError {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||
|
|
|
@ -520,15 +520,15 @@ struct MigrateFromDevice: View {
|
|||
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||
await MainActor.run {
|
||||
switch msg {
|
||||
case let .sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize):
|
||||
case let .result(.sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize)):
|
||||
if case let .uploadProgress(uploaded, total, _, _, _) = migrationState, uploaded != total {
|
||||
migrationState = .uploadProgress(uploadedBytes: sentSize, totalBytes: totalSize, fileId: fileTransferMeta.fileId, archivePath: archivePath, ctrl: ctrl)
|
||||
}
|
||||
case .sndFileRedirectStartXFTP:
|
||||
case .result(.sndFileRedirectStartXFTP):
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
migrationState = .linkCreation
|
||||
}
|
||||
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
|
||||
case let .result(.sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs)):
|
||||
let cfg = getNetCfg()
|
||||
let proxy: NetworkProxy? = if cfg.socksProxy == nil {
|
||||
nil
|
||||
|
@ -546,11 +546,11 @@ struct MigrateFromDevice: View {
|
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
migrationState = .linkShown(fileId: fileTransferMeta.fileId, link: data.addToLink(link: rcvURIs[0]), archivePath: archivePath, ctrl: ctrl)
|
||||
}
|
||||
case .sndFileError:
|
||||
case .result(.sndFileError):
|
||||
alert = .error(title: "Upload failed", error: "Check your internet connection and try again")
|
||||
migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
|
||||
default:
|
||||
logger.debug("unsupported event: \(msg.eventType)")
|
||||
logger.debug("unsupported event: \(msg.responseType)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -691,7 +691,7 @@ private struct PassphraseConfirmationView: View {
|
|||
migrationState = .uploadConfirmation
|
||||
}
|
||||
} catch let error {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse {
|
||||
if case .errorDatabase(.errorOpen(.errorNotADatabase)) = error as? ChatError {
|
||||
showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert)
|
||||
} else {
|
||||
alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(responseError(error)))
|
||||
|
@ -733,11 +733,11 @@ func chatStoppedView() -> some View {
|
|||
private class MigrationChatReceiver {
|
||||
let ctrl: chat_ctrl
|
||||
let databaseUrl: URL
|
||||
let processReceivedMsg: (ChatEvent) async -> Void
|
||||
let processReceivedMsg: (APIResult<ChatEvent>) async -> Void
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatEvent) async -> Void) {
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult<ChatEvent>) async -> Void) {
|
||||
self.ctrl = ctrl
|
||||
self.databaseUrl = databaseUrl
|
||||
self.processReceivedMsg = processReceivedMsg
|
||||
|
@ -752,11 +752,11 @@ private class MigrationChatReceiver {
|
|||
|
||||
func receiveMsgLoop() async {
|
||||
// TODO use function that has timeout
|
||||
if let msg: ChatEvent = await chatRecvMsg(ctrl) {
|
||||
if let msg: APIResult<ChatEvent> = await chatRecvMsg(ctrl) {
|
||||
Task {
|
||||
await TerminalItems.shared.add(.event(.now, msg))
|
||||
await TerminalItems.shared.addResult(msg)
|
||||
}
|
||||
logger.debug("processReceivedMsg: \(msg.eventType)")
|
||||
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||
await processReceivedMsg(msg)
|
||||
}
|
||||
if self.receiveMessages {
|
||||
|
|
|
@ -496,10 +496,10 @@ struct MigrateToDevice: View {
|
|||
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
|
||||
await MainActor.run {
|
||||
switch msg {
|
||||
case let .rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer):
|
||||
case let .result(.rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer)):
|
||||
migrationState = .downloadProgress(downloadedBytes: receivedSize, totalBytes: totalSize, fileId: rcvFileTransfer.fileId, link: link, archivePath: archivePath, ctrl: ctrl)
|
||||
MigrationToDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
|
||||
case .rcvStandaloneFileComplete:
|
||||
case .result(.rcvStandaloneFileComplete):
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
// User closed the whole screen before new state was saved
|
||||
if migrationState == nil {
|
||||
|
@ -509,14 +509,14 @@ struct MigrateToDevice: View {
|
|||
MigrationToDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
|
||||
}
|
||||
}
|
||||
case .rcvFileError:
|
||||
case .result(.rcvFileError):
|
||||
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
|
||||
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
case .chatError(_, .error(.noRcvFileUser)):
|
||||
case .error(.error(.noRcvFileUser)):
|
||||
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
|
||||
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
|
||||
default:
|
||||
logger.debug("unsupported event: \(msg.eventType)")
|
||||
logger.debug("unsupported event: \(msg.responseType)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -751,11 +751,11 @@ private func progressView() -> some View {
|
|||
private class MigrationChatReceiver {
|
||||
let ctrl: chat_ctrl
|
||||
let databaseUrl: URL
|
||||
let processReceivedMsg: (ChatEvent) async -> Void
|
||||
let processReceivedMsg: (APIResult<ChatEvent>) async -> Void
|
||||
private var receiveLoop: Task<Void, Never>?
|
||||
private var receiveMessages = true
|
||||
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (ChatEvent) async -> Void) {
|
||||
init(ctrl: chat_ctrl, databaseUrl: URL, _ processReceivedMsg: @escaping (APIResult<ChatEvent>) async -> Void) {
|
||||
self.ctrl = ctrl
|
||||
self.databaseUrl = databaseUrl
|
||||
self.processReceivedMsg = processReceivedMsg
|
||||
|
@ -772,9 +772,9 @@ private class MigrationChatReceiver {
|
|||
// TODO use function that has timeout
|
||||
if let msg = await chatRecvMsg(ctrl) {
|
||||
Task {
|
||||
await TerminalItems.shared.add(.event(.now, msg))
|
||||
await TerminalItems.shared.addResult(msg)
|
||||
}
|
||||
logger.debug("processReceivedMsg: \(msg.eventType)")
|
||||
logger.debug("processReceivedMsg: \(msg.responseType)")
|
||||
await processReceivedMsg(msg)
|
||||
}
|
||||
if self.receiveMessages {
|
||||
|
|
|
@ -236,15 +236,15 @@ private func showCreateProfileAlert(
|
|||
_ error: Error
|
||||
) {
|
||||
let m = ChatModel.shared
|
||||
switch error as? ChatResponse {
|
||||
case .chatCmdError(_, .errorStore(.duplicateName)),
|
||||
.chatCmdError(_, .error(.userExists)):
|
||||
switch error as? ChatError {
|
||||
case .errorStore(.duplicateName),
|
||||
.error(.userExists):
|
||||
if m.currentUser == nil {
|
||||
AlertManager.shared.showAlert(duplicateUserAlert)
|
||||
} else {
|
||||
showAlert(.duplicateUserError)
|
||||
}
|
||||
case .chatCmdError(_, .error(.invalidDisplayName)):
|
||||
case .error(.invalidDisplayName):
|
||||
if m.currentUser == nil {
|
||||
AlertManager.shared.showAlert(invalidDisplayNameAlert)
|
||||
} else {
|
||||
|
|
|
@ -456,12 +456,12 @@ struct ConnectDesktopView: View {
|
|||
}
|
||||
} catch let e {
|
||||
await MainActor.run {
|
||||
switch e as? ChatResponse {
|
||||
case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError
|
||||
case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError
|
||||
case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v)
|
||||
case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil)
|
||||
case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError
|
||||
switch e as? ChatError {
|
||||
case .errorRemoteCtrl(.badInvitation): alert = .badInvitationError
|
||||
case .error(.commandError): alert = .badInvitationError
|
||||
case let .errorRemoteCtrl(.badVersion(v)): alert = .badVersionError(version: v)
|
||||
case .errorAgent(.RCP(.version)): alert = .badVersionError(version: nil)
|
||||
case .errorAgent(.RCP(.ctrlAuth)): alert = .desktopDisconnectedError
|
||||
default: errorAlert(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -145,18 +145,18 @@ struct TerminalView: View {
|
|||
}
|
||||
|
||||
func consoleSendMessage() {
|
||||
let cmd = ChatCommand.string(composeState.message)
|
||||
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) {
|
||||
let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||
let resp: APIResult<ChatResponse2> = APIResult.error(ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty")))
|
||||
Task {
|
||||
await TerminalItems.shared.addCommand(.now, cmd, resp)
|
||||
await TerminalItems.shared.addCommand(.now, .string(composeState.message), resp)
|
||||
}
|
||||
} else {
|
||||
let cmd = composeState.message
|
||||
DispatchQueue.global().async {
|
||||
Task {
|
||||
composeState.inProgress = true
|
||||
_ = await chatSendCmd(cmd)
|
||||
composeState.inProgress = false
|
||||
await MainActor.run { composeState.inProgress = true }
|
||||
await sendTerminalCmd(cmd)
|
||||
await MainActor.run { composeState.inProgress = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -164,12 +164,38 @@ struct TerminalView: View {
|
|||
}
|
||||
}
|
||||
|
||||
func sendTerminalCmd(_ cmd: String) async {
|
||||
let start: Date = .now
|
||||
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) in
|
||||
let d = sendSimpleXCmdStr(cmd)
|
||||
Task {
|
||||
guard let d else {
|
||||
await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult<ChatResponse2>.error(.invalidJSON(json: nil)))
|
||||
return
|
||||
}
|
||||
let r0: APIResult<ChatResponse0> = decodeAPIResult(d)
|
||||
guard case .invalid = r0 else {
|
||||
await TerminalItems.shared.addCommand(start, .string(cmd), r0)
|
||||
return
|
||||
}
|
||||
let r1: APIResult<ChatResponse1> = decodeAPIResult(d)
|
||||
guard case .invalid = r1 else {
|
||||
await TerminalItems.shared.addCommand(start, .string(cmd), r1)
|
||||
return
|
||||
}
|
||||
let r2: APIResult<ChatResponse2> = decodeAPIResult(d)
|
||||
await TerminalItems.shared.addCommand(start, .string(cmd), r2)
|
||||
}
|
||||
cont.resume(returning: ())
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let chatModel = ChatModel()
|
||||
chatModel.terminalItems = [
|
||||
.resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")),
|
||||
.resp(.now, ChatResponse.response(type: "newChatItems", json: "{}"))
|
||||
.err(.now, APIResult<ChatResponse2>.invalid(type: "contactSubscribed", json: "{}".data(using: .utf8)!).unexpected),
|
||||
.err(.now, APIResult<ChatResponse2>.invalid(type: "newChatItems", json: "{}".data(using: .utf8)!).unexpected)
|
||||
]
|
||||
return NavigationView {
|
||||
TerminalView()
|
||||
|
|
|
@ -47,8 +47,7 @@ enum NSEChatCommand: ChatCmdProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
enum NSEChatResponse: Decodable, Error, ChatRespProtocol {
|
||||
case response(type: String, json: String)
|
||||
enum NSEChatResponse: Decodable, ChatAPIResult {
|
||||
case activeUser(user: User)
|
||||
case chatStarted
|
||||
case chatRunning
|
||||
|
@ -57,11 +56,9 @@ enum NSEChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case connNtfMessages(receivedMsgs: [NtfMsgInfo?])
|
||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
|
||||
case cmdOk(user_: UserRef?)
|
||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||
|
||||
var responseType: String {
|
||||
switch self {
|
||||
case let .response(type, _): "* \(type)"
|
||||
case .activeUser: "activeUser"
|
||||
case .chatStarted: "chatStarted"
|
||||
case .chatRunning: "chatRunning"
|
||||
|
@ -70,13 +67,11 @@ enum NSEChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case .connNtfMessages: "connNtfMessages"
|
||||
case .ntfMessage: "ntfMessage"
|
||||
case .cmdOk: "cmdOk"
|
||||
case .chatCmdError: "chatCmdError"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
switch self {
|
||||
case let .response(_, json): return json
|
||||
case let .activeUser(user): return String(describing: user)
|
||||
case .chatStarted: return noDetails
|
||||
case .chatRunning: return noDetails
|
||||
|
@ -85,60 +80,11 @@ enum NSEChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))"
|
||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||
case .cmdOk: return noDetails
|
||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
}
|
||||
}
|
||||
|
||||
var noDetails: String { "\(responseType): no details" }
|
||||
|
||||
static func chatResponse(_ s: String) -> NSEChatResponse {
|
||||
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)
|
||||
do {
|
||||
let r = try jsonDecoder.decode(APIResponse<NSEChatResponse>.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 jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if jResp.count == 2 && type == "_owsf" {
|
||||
type = jResp.allKeys[1] as? String
|
||||
}
|
||||
if type == "chatCmdError" {
|
||||
if let jError = jResp["chatCmdError"] as? NSDictionary {
|
||||
return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
json = serializeJSON(j, options: .prettyPrinted)
|
||||
}
|
||||
return NSEChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
var chatError: ChatError? {
|
||||
switch self {
|
||||
case let .chatCmdError(_, error): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
var chatErrorType: ChatErrorType? {
|
||||
switch self {
|
||||
case let .chatCmdError(_, .error(error)): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NSEChatEvent: Decodable, Error, ChatEventProtocol {
|
||||
case event(type: String, json: String)
|
||||
enum NSEChatEvent: Decodable, ChatAPIResult {
|
||||
case chatSuspended
|
||||
case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?)
|
||||
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest)
|
||||
|
@ -148,11 +94,9 @@ enum NSEChatEvent: Decodable, Error, ChatEventProtocol {
|
|||
case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer)
|
||||
case callInvitation(callInvitation: RcvCallInvitation)
|
||||
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
|
||||
case chatError(user_: UserRef?, chatError: ChatError)
|
||||
|
||||
var eventType: String {
|
||||
var responseType: String {
|
||||
switch self {
|
||||
case let .event(type, _): "* \(type)"
|
||||
case .chatSuspended: "chatSuspended"
|
||||
case .contactConnected: "contactConnected"
|
||||
case .receivedContactRequest: "receivedContactRequest"
|
||||
|
@ -162,13 +106,11 @@ enum NSEChatEvent: Decodable, Error, ChatEventProtocol {
|
|||
case .sndFileRcvCancelled: "sndFileRcvCancelled"
|
||||
case .callInvitation: "callInvitation"
|
||||
case .ntfMessage: "ntfMessage"
|
||||
case .chatError: "chatError"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
switch self {
|
||||
case let .event(_, json): return json
|
||||
case .chatSuspended: return noDetails
|
||||
case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact))
|
||||
case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest))
|
||||
|
@ -180,54 +122,6 @@ enum NSEChatEvent: Decodable, Error, ChatEventProtocol {
|
|||
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
case let .callInvitation(inv): return String(describing: inv)
|
||||
case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
|
||||
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
}
|
||||
}
|
||||
|
||||
var noDetails: String { "\(eventType): no details" }
|
||||
|
||||
static func chatEvent(_ s: String) -> NSEChatEvent {
|
||||
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)
|
||||
do {
|
||||
let r = try jsonDecoder.decode(APIResponse<NSEChatEvent>.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 jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if jResp.count == 2 && type == "_owsf" {
|
||||
type = jResp.allKeys[1] as? String
|
||||
}
|
||||
if type == "chatError" {
|
||||
if let jError = jResp["chatError"] as? NSDictionary {
|
||||
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
json = serializeJSON(j, options: .prettyPrinted)
|
||||
}
|
||||
return NSEChatEvent.event(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
var chatError: ChatError? {
|
||||
switch self {
|
||||
case let .chatError(_, error): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
var chatErrorType: ChatErrorType? {
|
||||
switch self {
|
||||
case let .chatError(_, .error(error)): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -774,12 +774,18 @@ func receiveMessages() async {
|
|||
}
|
||||
|
||||
func receiveMsg() async {
|
||||
if let msg = await chatRecvMsg() {
|
||||
switch await chatRecvMsg() {
|
||||
case let .result(msg):
|
||||
logger.debug("NotificationService receiveMsg: message")
|
||||
if let (id, ntf) = await receivedMsgNtf(msg) {
|
||||
logger.debug("NotificationService receiveMsg: notification")
|
||||
await NSEThreads.shared.processNotification(id, ntf)
|
||||
}
|
||||
case let .error(err):
|
||||
logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))")
|
||||
case let .invalid(type, _):
|
||||
logger.error("NotificationService receivedMsgNtf invalid: \(type)")
|
||||
case .none: ()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -789,9 +795,9 @@ func receiveMessages() async {
|
|||
}
|
||||
}
|
||||
|
||||
func chatRecvMsg() async -> NSEChatEvent? {
|
||||
func chatRecvMsg() async -> APIResult<NSEChatEvent>? {
|
||||
await withCheckedContinuation { cont in
|
||||
let resp: NSEChatEvent? = recvSimpleXMsg()
|
||||
let resp: APIResult<NSEChatEvent>? = recvSimpleXMsg()
|
||||
cont.resume(returning: resp)
|
||||
}
|
||||
}
|
||||
|
@ -800,7 +806,7 @@ private let isInChina = SKStorefront().countryCode == "CHN"
|
|||
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
|
||||
|
||||
func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? {
|
||||
logger.debug("NotificationService receivedMsgNtf: \(res.eventType)")
|
||||
logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
|
||||
switch res {
|
||||
case let .contactConnected(user, contact, _):
|
||||
return (contact.id, .contactConnected(user, contact))
|
||||
|
@ -845,11 +851,8 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)?
|
|||
case .chatSuspended:
|
||||
chatSuspended()
|
||||
return nil
|
||||
case let .chatError(_, err):
|
||||
logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))")
|
||||
return nil
|
||||
default:
|
||||
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.eventType)")
|
||||
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -868,14 +871,14 @@ func updateNetCfg() {
|
|||
}
|
||||
|
||||
func apiGetActiveUser() -> User? {
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.showActiveUser)
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.showActiveUser)
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)")
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(_, .error(.noActiveUser)):
|
||||
case let .result(.activeUser(user)): return user
|
||||
case .error(.error(.noActiveUser)):
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd no active user")
|
||||
return nil
|
||||
case let .chatCmdError(_, err):
|
||||
case let .error(err):
|
||||
logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))")
|
||||
return nil
|
||||
default:
|
||||
|
@ -885,39 +888,39 @@ func apiGetActiveUser() -> User? {
|
|||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.startChat(mainApp: false, enableSndFiles: false))
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.startChat(mainApp: false, enableSndFiles: false))
|
||||
switch r {
|
||||
case .chatStarted: return true
|
||||
case .chatRunning: return false
|
||||
default: throw r
|
||||
case .result(.chatStarted): return true
|
||||
case .result(.chatRunning): return false
|
||||
default: throw r.unexpected
|
||||
}
|
||||
}
|
||||
|
||||
func apiActivateChat() -> Bool {
|
||||
chatReopenStore()
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiActivateChat(restoreChat: false))
|
||||
if case .cmdOk = r { return true }
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiActivateChat(restoreChat: false))
|
||||
if case .result(.cmdOk) = r { return true }
|
||||
logger.error("NotificationService apiActivateChat error: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
func apiSuspendChat(timeoutMicroseconds: Int) -> Bool {
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
|
||||
if case .cmdOk = r { return true }
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
|
||||
if case .result(.cmdOk) = r { return true }
|
||||
logger.error("NotificationService apiSuspendChat error: \(String(describing: r))")
|
||||
return false
|
||||
}
|
||||
|
||||
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws {
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSetEncryptLocalFiles(enable: enable))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetEncryptLocalFiles(enable: enable))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [NtfConn]? {
|
||||
|
@ -925,11 +928,11 @@ func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [NtfConn]? {
|
|||
logger.debug("no active user")
|
||||
return nil
|
||||
}
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .ntfConns(ntfConns) = r {
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo))
|
||||
if case let .result(.ntfConns(ntfConns)) = r {
|
||||
logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)")
|
||||
return ntfConns
|
||||
} else if case let .chatCmdError(_, error) = r {
|
||||
} else if case let .error(error) = r {
|
||||
logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
|
||||
} else {
|
||||
logger.debug("apiGetNtfMessage ignored response: \(r.responseType) \(String.init(describing: r))")
|
||||
|
@ -943,12 +946,12 @@ func apiGetConnNtfMessages(connMsgReqs: [ConnMsgReq]) -> [NtfMsgInfo?]? {
|
|||
return nil
|
||||
}
|
||||
logger.debug("apiGetConnNtfMessages command: \(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs).cmdString)")
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs))
|
||||
if case let .connNtfMessages(receivedMsgs) = r {
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs))
|
||||
if case let .result(.connNtfMessages(receivedMsgs)) = r {
|
||||
logger.debug("apiGetConnNtfMessages response receivedMsgs: total \(receivedMsgs.count), expecting messages \(receivedMsgs.count { $0 != nil })")
|
||||
return receivedMsgs
|
||||
}
|
||||
logger.debug("apiGetConnNtfMessages error: \(responseError(r))")
|
||||
logger.debug("apiGetConnNtfMessages error: \(responseError(r.unexpected))")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -962,17 +965,17 @@ func getConnNtfMessage(connMsgReq: ConnMsgReq) -> NtfMsgInfo? {
|
|||
|
||||
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
|
||||
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||
logger.error("receiveFile error: \(responseError(r))")
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||
if case let .result(.rcvFileAccepted(_, chatItem)) = r { return chatItem }
|
||||
logger.error("receiveFile error: \(responseError(r.unexpected))")
|
||||
return nil
|
||||
}
|
||||
|
||||
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
|
||||
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted))
|
||||
if case .cmdOk = r { return }
|
||||
logger.error("setFileToReceive error: \(responseError(r))")
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
logger.error("setFileToReceive error: \(responseError(r.unexpected))")
|
||||
}
|
||||
|
||||
func autoReceiveFile(_ file: CIFile) -> ChatItem? {
|
||||
|
@ -989,9 +992,9 @@ func autoReceiveFile(_ file: CIFile) -> ChatItem? {
|
|||
}
|
||||
|
||||
func setNetworkConfig(_ cfg: NetCfg) throws {
|
||||
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSetNetworkConfig(networkConfig: cfg))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetNetworkConfig(networkConfig: cfg))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func defaultBestAttemptNtf(_ ntfConn: NtfConn) -> NSENotificationData {
|
||||
|
|
|
@ -13,52 +13,52 @@ import SimpleXChat
|
|||
let logger = Logger()
|
||||
|
||||
func apiGetActiveUser() throws -> User? {
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.showActiveUser)
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.showActiveUser)
|
||||
switch r {
|
||||
case let .activeUser(user): return user
|
||||
case .chatCmdError(_, .error(.noActiveUser)): return nil
|
||||
default: throw r
|
||||
case let .result(.activeUser(user)): return user
|
||||
case .error(.error(.noActiveUser)): return nil
|
||||
default: throw r.unexpected
|
||||
}
|
||||
}
|
||||
|
||||
func apiStartChat() throws -> Bool {
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.startChat(mainApp: false, enableSndFiles: true))
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.startChat(mainApp: false, enableSndFiles: true))
|
||||
switch r {
|
||||
case .chatStarted: return true
|
||||
case .chatRunning: return false
|
||||
default: throw r
|
||||
case .result(.chatStarted): return true
|
||||
case .result(.chatRunning): return false
|
||||
default: throw r.unexpected
|
||||
}
|
||||
}
|
||||
|
||||
func apiSetNetworkConfig(_ cfg: NetCfg) throws {
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetNetworkConfig(networkConfig: cfg))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetNetworkConfig(networkConfig: cfg))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws {
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiSetEncryptLocalFiles(_ enable: Bool) throws {
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetEncryptLocalFiles(enable: enable))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetEncryptLocalFiles(enable: enable))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiGetChats(userId: User.ID) throws -> Array<ChatData> {
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiGetChats(userId: userId))
|
||||
if case let .apiChats(user: _, chats: chats) = r { return chats }
|
||||
throw r
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiGetChats(userId: userId))
|
||||
if case let .result(.apiChats(user: _, chats: chats)) = r { return chats }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiSendMessages(
|
||||
chatInfo: ChatInfo,
|
||||
composedMessages: [ComposedMessage]
|
||||
) throws -> [AChatItem] {
|
||||
let r: SEChatResponse = sendSimpleXCmd(
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(
|
||||
chatInfo.chatType == .local
|
||||
? SEChatCommand.apiCreateChatItems(
|
||||
noteFolderId: chatInfo.apiId,
|
||||
|
@ -72,33 +72,33 @@ func apiSendMessages(
|
|||
composedMessages: composedMessages
|
||||
)
|
||||
)
|
||||
if case let .newChatItems(_, chatItems) = r {
|
||||
if case let .result(.newChatItems(_, chatItems)) = r {
|
||||
return chatItems
|
||||
} else {
|
||||
for composedMessage in composedMessages {
|
||||
if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) }
|
||||
}
|
||||
throw r
|
||||
throw r.unexpected
|
||||
}
|
||||
}
|
||||
|
||||
func apiActivateChat() throws {
|
||||
chatReopenStore()
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false))
|
||||
if case .cmdOk = r { return }
|
||||
throw r
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false))
|
||||
if case .result(.cmdOk) = r { return }
|
||||
throw r.unexpected
|
||||
}
|
||||
|
||||
func apiSuspendChat(expired: Bool) {
|
||||
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000))
|
||||
let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000))
|
||||
// Block until `chatSuspended` received or 3 seconds has passed
|
||||
var suspended = false
|
||||
if case .cmdOk = r, !expired {
|
||||
if case .result(.cmdOk) = r, !expired {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
while CFAbsoluteTimeGetCurrent() - startTime < 3 {
|
||||
let msg: SEChatEvent? = recvSimpleXMsg(messageTimeout: 3_500000)
|
||||
let msg: APIResult<SEChatEvent>? = recvSimpleXMsg(messageTimeout: 3_500000)
|
||||
switch msg {
|
||||
case .chatSuspended:
|
||||
case .result(.chatSuspended):
|
||||
suspended = false
|
||||
break
|
||||
default: continue
|
||||
|
@ -106,7 +106,7 @@ func apiSuspendChat(expired: Bool) {
|
|||
}
|
||||
}
|
||||
if !suspended {
|
||||
let _r1: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: 0))
|
||||
let _r1: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: 0))
|
||||
}
|
||||
logger.debug("close store")
|
||||
chatCloseStore()
|
||||
|
@ -151,32 +151,27 @@ enum SEChatCommand: ChatCmdProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
enum SEChatResponse: Decodable, Error, ChatRespProtocol {
|
||||
case response(type: String, json: String)
|
||||
enum SEChatResponse: Decodable, ChatAPIResult {
|
||||
case activeUser(user: User)
|
||||
case chatStarted
|
||||
case chatRunning
|
||||
case apiChats(user: UserRef, chats: [ChatData])
|
||||
case newChatItems(user: UserRef, chatItems: [AChatItem])
|
||||
case cmdOk(user_: UserRef?)
|
||||
case chatCmdError(user_: UserRef?, chatError: ChatError)
|
||||
|
||||
var responseType: String {
|
||||
switch self {
|
||||
case let .response(type, _): "* \(type)"
|
||||
case .activeUser: "activeUser"
|
||||
case .chatStarted: "chatStarted"
|
||||
case .chatRunning: "chatRunning"
|
||||
case .apiChats: "apiChats"
|
||||
case .newChatItems: "newChatItems"
|
||||
case .cmdOk: "cmdOk"
|
||||
case .chatCmdError: "chatCmdError"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
switch self {
|
||||
case let .response(_, json): return json
|
||||
case let .activeUser(user): return String(describing: user)
|
||||
case .chatStarted: return noDetails
|
||||
case .chatRunning: return noDetails
|
||||
|
@ -185,88 +180,39 @@ enum SEChatResponse: Decodable, Error, ChatRespProtocol {
|
|||
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
|
||||
return withUser(u, itemsString)
|
||||
case .cmdOk: return noDetails
|
||||
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
}
|
||||
}
|
||||
|
||||
var noDetails: String { "\(responseType): no details" }
|
||||
|
||||
static func chatResponse(_ s: String) -> SEChatResponse {
|
||||
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)
|
||||
do {
|
||||
let r = try jsonDecoder.decode(APIResponse<SEChatResponse>.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 jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if jResp.count == 2 && type == "_owsf" {
|
||||
type = jResp.allKeys[1] as? String
|
||||
}
|
||||
if type == "apiChats" {
|
||||
if let r = parseApiChats(jResp) {
|
||||
return .apiChats(user: r.user, chats: r.chats)
|
||||
}
|
||||
} else if type == "chatCmdError" {
|
||||
if let jError = jResp["chatCmdError"] as? NSDictionary {
|
||||
return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
json = serializeJSON(j, options: .prettyPrinted)
|
||||
}
|
||||
return SEChatResponse.response(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
|
||||
var chatError: ChatError? {
|
||||
switch self {
|
||||
case let .chatCmdError(_, error): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
var chatErrorType: ChatErrorType? {
|
||||
switch self {
|
||||
case let .chatCmdError(_, .error(error)): error
|
||||
default: nil
|
||||
static func fallbackResult(_ type: String, _ json: NSDictionary) -> SEChatResponse? {
|
||||
if type == "apiChats", let r = parseApiChats(json) {
|
||||
.apiChats(user: r.user, chats: r.chats)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SEChatEvent: Decodable, Error, ChatEventProtocol {
|
||||
case event(type: String, json: String)
|
||||
enum SEChatEvent: Decodable, ChatAPIResult {
|
||||
case chatSuspended
|
||||
case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
|
||||
case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
|
||||
case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem])
|
||||
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
|
||||
case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
|
||||
case chatError(user_: UserRef?, chatError: ChatError)
|
||||
|
||||
var eventType: String {
|
||||
var responseType: String {
|
||||
switch self {
|
||||
case let .event(type, _): "* \(type)"
|
||||
case .chatSuspended: "chatSuspended"
|
||||
case .sndFileProgressXFTP: "sndFileProgressXFTP"
|
||||
case .sndFileCompleteXFTP: "sndFileCompleteXFTP"
|
||||
case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated"
|
||||
case .sndFileError: "sndFileError"
|
||||
case .sndFileWarning: "sndFileWarning"
|
||||
case .chatError: "chatError"
|
||||
}
|
||||
}
|
||||
|
||||
var details: String {
|
||||
switch self {
|
||||
case let .event(_, json): return json
|
||||
case .chatSuspended: return noDetails
|
||||
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
|
||||
case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
|
||||
|
@ -275,53 +221,6 @@ enum SEChatEvent: Decodable, Error, ChatEventProtocol {
|
|||
return withUser(u, itemsString)
|
||||
case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))")
|
||||
case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))")
|
||||
case let .chatError(u, chatError): return withUser(u, String(describing: chatError))
|
||||
}
|
||||
}
|
||||
|
||||
var noDetails: String { "\(eventType): no details" }
|
||||
|
||||
static func chatEvent(_ s: String) -> SEChatEvent {
|
||||
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)
|
||||
do {
|
||||
let r = try jsonDecoder.decode(APIResponse<SEChatEvent>.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 jResp = j["resp"] as? NSDictionary, jResp.count == 1 || jResp.count == 2 {
|
||||
type = jResp.allKeys[0] as? String
|
||||
if jResp.count == 2 && type == "_owsf" {
|
||||
type = jResp.allKeys[1] as? String
|
||||
}
|
||||
if type == "chatError" {
|
||||
if let jError = jResp["chatError"] as? NSDictionary {
|
||||
return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
json = serializeJSON(j, options: .prettyPrinted)
|
||||
}
|
||||
return SEChatEvent.event(type: type ?? "invalid", json: json ?? s)
|
||||
}
|
||||
var chatError: ChatError? {
|
||||
switch self {
|
||||
case let .chatError(_, error): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
var chatErrorType: ChatErrorType? {
|
||||
switch self {
|
||||
case let .chatError(_, .error(error)): error
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -303,9 +303,9 @@ class ShareModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
let r: SEChatEvent? = recvSimpleXMsg(messageTimeout: 1_000_000)
|
||||
let r: APIResult<SEChatEvent>? = recvSimpleXMsg(messageTimeout: 1_000_000)
|
||||
switch r {
|
||||
case let .sndFileProgressXFTP(_, ci, _, sentSize, totalSize):
|
||||
case let .result(.sndFileProgressXFTP(_, ci, _, sentSize, totalSize)):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
networkTimeout = CFAbsoluteTimeGetCurrent()
|
||||
await MainActor.run {
|
||||
|
@ -314,14 +314,14 @@ class ShareModel: ObservableObject {
|
|||
bottomBar = .loadingBar(progress: progress)
|
||||
}
|
||||
}
|
||||
case let .sndFileCompleteXFTP(_, ci, _):
|
||||
case let .result(.sndFileCompleteXFTP(_, ci, _)):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if isGroupChat {
|
||||
await MainActor.run { bottomBar = .loadingSpinner }
|
||||
}
|
||||
await ch.completeFile()
|
||||
if await !ch.isRunning { break }
|
||||
case let .chatItemsStatusesUpdated(_, chatItems):
|
||||
case let .result(.chatItemsStatusesUpdated(_, chatItems)):
|
||||
guard let ci = chatItems.last else { continue }
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if let (title, message) = ci.chatItem.meta.itemStatus.statusInfo {
|
||||
|
@ -343,15 +343,15 @@ class ShareModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
case let .sndFileError(_, ci, _, errorMessage):
|
||||
case let .result(.sndFileError(_, ci, _, errorMessage)):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if let ci { cleanupFile(ci) }
|
||||
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)")
|
||||
case let .sndFileWarning(_, ci, _, errorMessage):
|
||||
case let .result(.sndFileWarning(_, ci, _, errorMessage)):
|
||||
guard isMessage(for: ci) else { continue }
|
||||
if let ci { cleanupFile(ci) }
|
||||
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)")
|
||||
case let .chatError(_, chatError):
|
||||
case let .error(chatError):
|
||||
return ErrorAlert(chatError)
|
||||
default: continue
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
|
|||
var cConfirm = confirm.rawValue.cString(using: .utf8)!
|
||||
// the last parameter of chat_migrate_init is used to return the pointer to chat controller
|
||||
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, backgroundMode ? 1 : 0, &chatController)!
|
||||
let dbRes = dbMigrationResult(fromCString(cjson))
|
||||
let dbRes = dbMigrationResult(dataFromCString(cjson))
|
||||
let encrypted = dbKey != ""
|
||||
let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey)
|
||||
let result = (encrypted, keychainErr ? .errorKeychain : dbRes)
|
||||
|
@ -63,7 +63,7 @@ public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation
|
|||
var cKey = dbKey.cString(using: .utf8)!
|
||||
var cConfirm = confirmation.rawValue.cString(using: .utf8)!
|
||||
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &temporaryController)!
|
||||
return (dbMigrationResult(fromCString(cjson)), temporaryController)
|
||||
return (dbMigrationResult(dataFromCString(cjson)), temporaryController)
|
||||
}
|
||||
|
||||
public func chatInitControllerRemovingDatabases() {
|
||||
|
@ -110,27 +110,42 @@ public func resetChatCtrl() {
|
|||
migrationResult = nil
|
||||
}
|
||||
|
||||
public func sendSimpleXCmd<CR: ChatRespProtocol>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> CR {
|
||||
var c = cmd.cmdString.cString(using: .utf8)!
|
||||
let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)!
|
||||
return CR.chatResponse(fromCString(cjson))
|
||||
@inline(__always)
|
||||
public func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult<R> {
|
||||
if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl) {
|
||||
decodeAPIResult(d)
|
||||
} else {
|
||||
APIResult.error(.invalidJSON(json: nil))
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil) -> Data? {
|
||||
var c = cmd.cString(using: .utf8)!
|
||||
return if let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c) {
|
||||
dataFromCString(cjson)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
// in microseconds
|
||||
public let MESSAGE_TIMEOUT: Int32 = 15_000_000
|
||||
|
||||
public func recvSimpleXMsg<CEvt: ChatEventProtocol>(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> CEvt? {
|
||||
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) {
|
||||
let s = fromCString(cjson)
|
||||
return s == "" ? nil : CEvt.chatEvent(s)
|
||||
@inline(__always)
|
||||
public func recvSimpleXMsg<R: ChatAPIResult>(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> APIResult<R>? {
|
||||
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout),
|
||||
let d = dataFromCString(cjson) {
|
||||
decodeAPIResult(d)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
|
||||
var c = s.cString(using: .utf8)!
|
||||
if let cjson = chat_parse_markdown(&c) {
|
||||
if let d = fromCString(cjson).data(using: .utf8) {
|
||||
if let d = dataFromCString(cjson) {
|
||||
do {
|
||||
let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d)
|
||||
return r.formattedText
|
||||
|
@ -154,7 +169,7 @@ struct ParsedMarkdown: Decodable {
|
|||
public func parseServerAddress(_ s: String) -> ServerAddress? {
|
||||
var c = s.cString(using: .utf8)!
|
||||
if let cjson = chat_parse_server(&c) {
|
||||
if let d = fromCString(cjson).data(using: .utf8) {
|
||||
if let d = dataFromCString(cjson) {
|
||||
do {
|
||||
let r = try jsonDecoder.decode(ParsedServerAddress.self, from: d)
|
||||
return r.serverAddress
|
||||
|
@ -171,12 +186,33 @@ struct ParsedServerAddress: Decodable {
|
|||
var parseError: String
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
|
||||
let s = String.init(cString: c)
|
||||
free(c)
|
||||
return s
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public func dataFromCString(_ c: UnsafeMutablePointer<CChar>) -> Data? {
|
||||
let len = strlen(c)
|
||||
if len > 0 {
|
||||
return Data(bytesNoCopy: c, count: len, deallocator: .free)
|
||||
} else {
|
||||
free(c)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public func dataToString(_ d: Data?) -> String {
|
||||
if let d {
|
||||
String(data: d, encoding: .utf8) ?? "invalid string"
|
||||
} else {
|
||||
"no data"
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeUser_(_ jDict: NSDictionary) -> UserRef? {
|
||||
if let user_ = jDict["user_"] {
|
||||
try? decodeObject(user_ as Any)
|
||||
|
@ -185,7 +221,7 @@ public func decodeUser_(_ jDict: NSDictionary) -> UserRef? {
|
|||
}
|
||||
}
|
||||
|
||||
public func errorJson(_ jDict: NSDictionary) -> String? {
|
||||
public func errorJson(_ jDict: NSDictionary) -> Data? {
|
||||
if let chatError = jDict["chatError"] {
|
||||
serializeJSON(chatError)
|
||||
} else {
|
||||
|
@ -197,7 +233,11 @@ public func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatD
|
|||
let jChatDict = jChat as! NSDictionary
|
||||
let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!)
|
||||
let chatStats: ChatStats = try decodeObject(jChatDict["chatStats"]!)
|
||||
let navInfo: NavigationInfo = jNavInfo == nil ? NavigationInfo() : try decodeObject((jNavInfo as! NSDictionary)["navInfo"]!)
|
||||
let navInfo: NavigationInfo = if let jNavInfo = jNavInfo as? NSDictionary, let jNav = jNavInfo["navInfo"] {
|
||||
try decodeObject(jNav)
|
||||
} else {
|
||||
NavigationInfo()
|
||||
}
|
||||
let jChatItems = jChatDict["chatItems"] as! NSArray
|
||||
let chatItems = jChatItems.map { jCI in
|
||||
if let ci: ChatItem = try? decodeObject(jCI) {
|
||||
|
@ -206,16 +246,18 @@ public func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatD
|
|||
return ChatItem.invalidJSON(
|
||||
chatDir: decodeProperty(jCI, "chatDir"),
|
||||
meta: decodeProperty(jCI, "meta"),
|
||||
json: serializeJSON(jCI, options: .prettyPrinted) ?? ""
|
||||
json: serializeJSON(jCI, options: .prettyPrinted)
|
||||
)
|
||||
}
|
||||
return (ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats), navInfo)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public func decodeObject<T: Decodable>(_ obj: Any) throws -> T {
|
||||
try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj))
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? {
|
||||
if let jProp = (obj as? NSDictionary)?[prop] {
|
||||
return try? decodeObject(jProp)
|
||||
|
@ -223,28 +265,52 @@ func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? {
|
|||
return nil
|
||||
}
|
||||
|
||||
public func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> String? {
|
||||
if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) {
|
||||
return String(decoding: d, as: UTF8.self)
|
||||
@inline(__always)
|
||||
func getOWSF(_ obj: NSDictionary, _ prop: NSString) -> (type: String, object: NSDictionary)? {
|
||||
if let j = obj[prop] as? NSDictionary, j.count == 1 || j.count == 2 {
|
||||
var type = j.allKeys[0] as? String
|
||||
if j.count == 2 && type == "_owsf" {
|
||||
type = j.allKeys[1] as? String
|
||||
}
|
||||
if let type {
|
||||
return (type, j)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> Data? {
|
||||
if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) {
|
||||
dataPrefix(d)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
let MAX_JSON_VIEW_LENGTH = 2048
|
||||
|
||||
@inline(__always)
|
||||
public func dataPrefix(_ d: Data) -> Data {
|
||||
d.count > MAX_JSON_VIEW_LENGTH
|
||||
? Data(d.prefix(MAX_JSON_VIEW_LENGTH))
|
||||
: d
|
||||
}
|
||||
|
||||
public func responseError(_ err: Error) -> String {
|
||||
if let r = err as? ChatRespProtocol {
|
||||
if let e = r.chatError {
|
||||
chatErrorString(e)
|
||||
} else {
|
||||
"\(String(describing: r.responseType)), details: \(String(describing: r.details))"
|
||||
}
|
||||
if let e = err as? ChatError {
|
||||
chatErrorString(e)
|
||||
} else {
|
||||
String(describing: err)
|
||||
}
|
||||
}
|
||||
|
||||
public func chatErrorString(_ err: ChatError) -> String {
|
||||
if case let .invalidJSON(json) = err { return json }
|
||||
return String(describing: err)
|
||||
switch err {
|
||||
case let .invalidJSON(json): dataToString(json)
|
||||
case let .unexpectedResult(type): "unexpected result: \(type)"
|
||||
default: String(describing: err)
|
||||
}
|
||||
}
|
||||
|
||||
public enum DBMigrationResult: Decodable, Equatable {
|
||||
|
@ -283,15 +349,15 @@ public enum MTRError: Decodable, Equatable {
|
|||
case different(appMigration: String, dbMigration: String)
|
||||
}
|
||||
|
||||
func dbMigrationResult(_ s: String) -> DBMigrationResult {
|
||||
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)
|
||||
do {
|
||||
return try jsonDecoder.decode(DBMigrationResult.self, from: d)
|
||||
} catch let error {
|
||||
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
||||
return .unknown(json: s)
|
||||
func dbMigrationResult(_ d: Data?) -> DBMigrationResult {
|
||||
if let d {
|
||||
do {
|
||||
return try jsonDecoder.decode(DBMigrationResult.self, from: d)
|
||||
} catch let error {
|
||||
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
|
||||
return .unknown(json: dataToString(d))
|
||||
}
|
||||
} else {
|
||||
return .unknown(json: "no data")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,30 +17,117 @@ public protocol ChatCmdProtocol {
|
|||
var cmdString: String { get }
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public func onOff(_ b: Bool) -> String {
|
||||
b ? "on" : "off"
|
||||
}
|
||||
|
||||
public struct APIResponse<ChatRespProtocol: Decodable>: Decodable {
|
||||
public var resp: ChatRespProtocol
|
||||
public enum APIResult<R>: Decodable where R: Decodable, R: ChatAPIResult {
|
||||
case result(R)
|
||||
case error(ChatError)
|
||||
case invalid(type: String, json: Data)
|
||||
|
||||
public var responseType: String {
|
||||
switch self {
|
||||
case let .result(r): r.responseType
|
||||
case let .error(e): "error \(e.errorType)"
|
||||
case let .invalid(type, _): "* \(type)"
|
||||
}
|
||||
}
|
||||
|
||||
public var unexpected: ChatError {
|
||||
switch self {
|
||||
case let .result(r): .unexpectedResult(type: r.responseType)
|
||||
case let .error(e): e
|
||||
case let .invalid(type, _): .unexpectedResult(type: "* \(type)")
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
if container.contains(.result) {
|
||||
let result = try container.decode(R.self, forKey: .result)
|
||||
self = .result(result)
|
||||
} else {
|
||||
let error = try container.decode(ChatError.self, forKey: .error)
|
||||
self = .error(error)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case result, error
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ChatRespProtocol: Decodable, Error {
|
||||
public protocol ChatAPIResult: Decodable {
|
||||
var responseType: String { get }
|
||||
var details: String { get }
|
||||
static func chatResponse(_ s: String) -> Self
|
||||
var chatError: ChatError? { get }
|
||||
var chatErrorType: ChatErrorType? { get }
|
||||
static func fallbackResult(_ type: String, _ json: NSDictionary) -> Self?
|
||||
}
|
||||
|
||||
public protocol ChatEventProtocol: Decodable, Error {
|
||||
var eventType: String { get }
|
||||
var details: String { get }
|
||||
static func chatEvent(_ s: String) -> Self
|
||||
var chatError: ChatError? { get }
|
||||
var chatErrorType: ChatErrorType? { get }
|
||||
extension ChatAPIResult {
|
||||
public var noDetails: String { "\(self.responseType): no details" }
|
||||
|
||||
@inline(__always)
|
||||
public static func fallbackResult(_ type: String, _ json: NSDictionary) -> Self? {
|
||||
nil
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
public var unexpected: ChatError {
|
||||
.unexpectedResult(type: self.responseType)
|
||||
}
|
||||
}
|
||||
|
||||
public func decodeAPIResult<R: ChatAPIResult>(_ d: Data) -> APIResult<R> {
|
||||
// print("decodeAPIResult \(String(describing: R.self))")
|
||||
do {
|
||||
// return try withStackSizeLimit { try jsonDecoder.decode(APIResult<R>.self, from: d) }
|
||||
return try jsonDecoder.decode(APIResult<R>.self, from: d)
|
||||
} catch {}
|
||||
if let j = try? JSONSerialization.jsonObject(with: d) as? NSDictionary {
|
||||
if let (_, jErr) = getOWSF(j, "error") {
|
||||
return APIResult.error(.invalidJSON(json: errorJson(jErr))) as APIResult<R>
|
||||
} else if let (type, jRes) = getOWSF(j, "result") {
|
||||
return if let r = R.fallbackResult(type, jRes) {
|
||||
APIResult.result(r)
|
||||
} else {
|
||||
APIResult.invalid(type: type, json: dataPrefix(d))
|
||||
}
|
||||
}
|
||||
}
|
||||
return APIResult.invalid(type: "invalid", json: dataPrefix(d))
|
||||
}
|
||||
|
||||
// Default stack size for the main thread is 1mb, for secondary threads - 512 kb.
|
||||
// This function can be used to test what size is used (or to increase available stack size).
|
||||
// Stack size must be a multiple of system page size (16kb).
|
||||
//private let stackSizeLimit: Int = 256 * 1024
|
||||
//
|
||||
//private func withStackSizeLimit<T>(_ f: @escaping () throws -> T) throws -> T {
|
||||
// let semaphore = DispatchSemaphore(value: 0)
|
||||
// var result: Result<T, Error>?
|
||||
// let thread = Thread {
|
||||
// do {
|
||||
// result = .success(try f())
|
||||
// } catch {
|
||||
// result = .failure(error)
|
||||
// }
|
||||
// semaphore.signal()
|
||||
// }
|
||||
//
|
||||
// thread.stackSize = stackSizeLimit
|
||||
// thread.qualityOfService = Thread.current.qualityOfService
|
||||
// thread.start()
|
||||
//
|
||||
// semaphore.wait()
|
||||
//
|
||||
// switch result! {
|
||||
// case let .success(r): return r
|
||||
// case let .failure(e): throw e
|
||||
// }
|
||||
//}
|
||||
|
||||
public func parseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [ChatData])? {
|
||||
if let jApiChats = jResp["apiChats"] as? NSDictionary,
|
||||
let user: UserRef = try? decodeObject(jApiChats["user"] as Any),
|
||||
|
@ -49,7 +136,7 @@ public func parseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [Chat
|
|||
if let chatData = try? parseChatData(jChat) {
|
||||
return chatData.0
|
||||
}
|
||||
return ChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted) ?? "")
|
||||
return ChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted))
|
||||
}
|
||||
return (user, chats)
|
||||
} else {
|
||||
|
@ -553,13 +640,26 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
|
|||
encodeJSON(value).cString(using: .utf8)!
|
||||
}
|
||||
|
||||
public enum ChatError: Decodable, Hashable {
|
||||
public enum ChatError: Decodable, Hashable, Error {
|
||||
case error(errorType: ChatErrorType)
|
||||
case errorAgent(agentError: AgentErrorType)
|
||||
case errorStore(storeError: StoreError)
|
||||
case errorDatabase(databaseError: DatabaseError)
|
||||
case errorRemoteCtrl(remoteCtrlError: RemoteCtrlError)
|
||||
case invalidJSON(json: String)
|
||||
case invalidJSON(json: Data?) // additional case used to pass errors that failed to parse
|
||||
case unexpectedResult(type: String) // additional case used to pass unexpected responses
|
||||
|
||||
public var errorType: String {
|
||||
switch self {
|
||||
case .error: "chat"
|
||||
case .errorAgent: "agent"
|
||||
case .errorStore: "store"
|
||||
case .errorDatabase: "database"
|
||||
case .errorRemoteCtrl: "remoteCtrl"
|
||||
case .invalidJSON: "invalid"
|
||||
case let .unexpectedResult(type): "! \(type)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatErrorType: Decodable, Hashable {
|
||||
|
|
|
@ -1201,7 +1201,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
|
|||
case local(noteFolder: NoteFolder)
|
||||
case contactRequest(contactRequest: UserContactRequest)
|
||||
case contactConnection(contactConnection: PendingContactConnection)
|
||||
case invalidJSON(json: String)
|
||||
case invalidJSON(json: Data?)
|
||||
|
||||
private static let invalidChatName = NSLocalizedString("invalid chat", comment: "invalid chat data")
|
||||
|
||||
|
@ -1589,7 +1589,7 @@ public struct ChatData: Decodable, Identifiable, Hashable, ChatLike {
|
|||
self.chatStats = chatStats
|
||||
}
|
||||
|
||||
public static func invalidJSON(_ json: String) -> ChatData {
|
||||
public static func invalidJSON(_ json: Data?) -> ChatData {
|
||||
ChatData(
|
||||
chatInfo: .invalidJSON(json: json),
|
||||
chatItems: [],
|
||||
|
@ -2905,7 +2905,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
|
|||
return item
|
||||
}
|
||||
|
||||
public static func invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: String) -> ChatItem {
|
||||
public static func invalidJSON(chatDir: CIDirection?, meta: CIMeta?, json: Data?) -> ChatItem {
|
||||
ChatItem(
|
||||
chatDir: chatDir ?? .directSnd,
|
||||
meta: meta ?? .invalidJSON,
|
||||
|
@ -3352,7 +3352,7 @@ public enum CIContent: Decodable, ItemContent, Hashable {
|
|||
case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo)
|
||||
case sndGroupE2EEInfo(e2eeInfo: E2EEInfo)
|
||||
case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo)
|
||||
case invalidJSON(json: String)
|
||||
case invalidJSON(json: Data?)
|
||||
|
||||
public var text: String {
|
||||
get {
|
||||
|
|
|
@ -18,10 +18,10 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs {
|
|||
memcpy(ptr, (data as NSData).bytes, data.count)
|
||||
var cPath = path.cString(using: .utf8)!
|
||||
let cjson = chat_write_file(getChatCtrl(), &cPath, ptr, Int32(data.count))!
|
||||
let d = fromCString(cjson).data(using: .utf8)!
|
||||
let d = dataFromCString(cjson)! // TODO [unsafe]
|
||||
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
|
||||
case let .result(cfArgs): return cfArgs
|
||||
case let .error(err): throw RuntimeError(err)
|
||||
case let .error(err): throw RuntimeError(err) // TODO [unsafe]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,10 +51,10 @@ public func encryptCryptoFile(fromPath: String, toPath: String) throws -> Crypto
|
|||
var cFromPath = fromPath.cString(using: .utf8)!
|
||||
var cToPath = toPath.cString(using: .utf8)!
|
||||
let cjson = chat_encrypt_file(getChatCtrl(), &cFromPath, &cToPath)!
|
||||
let d = fromCString(cjson).data(using: .utf8)!
|
||||
let d = dataFromCString(cjson)! // TODO [unsafe]
|
||||
switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
|
||||
case let .result(cfArgs): return cfArgs
|
||||
case let .error(err): throw RuntimeError(err)
|
||||
case let .error(err): throw RuntimeError(err) // TODO [unsafe]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -37,22 +37,18 @@ public struct ErrorAlert: Error {
|
|||
}
|
||||
|
||||
public init(_ error: any Error) {
|
||||
self = if let chatResponse = error as? ChatRespProtocol {
|
||||
ErrorAlert(chatResponse)
|
||||
self = if let e = error as? ChatError {
|
||||
ErrorAlert(e)
|
||||
} else {
|
||||
ErrorAlert("\(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
public init(_ chatError: ChatError) {
|
||||
self = ErrorAlert("\(chatErrorString(chatError))")
|
||||
}
|
||||
|
||||
public init(_ chatResponse: ChatRespProtocol) {
|
||||
self = if let networkErrorAlert = getNetworkErrorAlert(chatResponse) {
|
||||
self = if let networkErrorAlert = getNetworkErrorAlert(chatError) {
|
||||
networkErrorAlert
|
||||
} else {
|
||||
ErrorAlert("\(responseError(chatResponse))")
|
||||
ErrorAlert("\(chatErrorString(chatError))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,8 +90,8 @@ extension View {
|
|||
}
|
||||
}
|
||||
|
||||
public func getNetworkErrorAlert(_ r: ChatRespProtocol) -> ErrorAlert? {
|
||||
switch r.chatError {
|
||||
public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? {
|
||||
switch e {
|
||||
case let .errorAgent(.BROKER(addr, .TIMEOUT)):
|
||||
ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
|
||||
case let .errorAgent(.BROKER(addr, .NETWORK)):
|
||||
|
|
|
@ -229,5 +229,5 @@ fun isMediaIntent(intent: Intent): Boolean =
|
|||
// val str: String = """
|
||||
// """.trimIndent()
|
||||
//
|
||||
// println(json.decodeFromString<APIResponse>(str))
|
||||
// println(json.decodeFromString<APIResult>(str))
|
||||
//}
|
||||
|
|
|
@ -38,6 +38,7 @@ import java.net.URI
|
|||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.random.Random
|
||||
import kotlin.time.*
|
||||
|
@ -1396,19 +1397,21 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
}
|
||||
|
||||
@Serializable @SerialName("invalidJSON")
|
||||
class InvalidJSON(val json: String): ChatInfo() {
|
||||
class InvalidJSON(
|
||||
val json: String,
|
||||
override val apiId: Long = -idGenerator.getAndIncrement(),
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val updatedAt: Instant = Clock.System.now()
|
||||
): ChatInfo() {
|
||||
override val chatType get() = ChatType.Direct
|
||||
override val localDisplayName get() = invalidChatName
|
||||
override val id get() = ""
|
||||
override val apiId get() = 0L
|
||||
override val id get() = "?$apiId"
|
||||
override val ready get() = false
|
||||
override val chatDeleted get() = false
|
||||
override val sendMsgEnabled get() = false
|
||||
override val incognito get() = false
|
||||
override fun featureEnabled(feature: ChatFeature) = false
|
||||
override val timedMessagesTTL: Int? get() = null
|
||||
override val createdAt get() = Clock.System.now()
|
||||
override val updatedAt get() = Clock.System.now()
|
||||
override val displayName get() = invalidChatName
|
||||
override val fullName get() = invalidChatName
|
||||
override val image get() = null
|
||||
|
@ -1416,6 +1419,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
|
|||
|
||||
companion object {
|
||||
private val invalidChatName = generalGetString(MR.strings.invalid_chat)
|
||||
private val idGenerator = AtomicLong(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -48,7 +48,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
|||
val prefPerformLA = chatModel.controller.appPrefs.performLA.get()
|
||||
val s = composeState.value.message
|
||||
if (s.text.startsWith("/sql") && (!prefPerformLA || !developerTools)) {
|
||||
val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty")))
|
||||
val resp = API.Error(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty")))
|
||||
chatModel.addTerminalItem(TerminalItem.cmd(null, CC.Console(s.text)))
|
||||
chatModel.addTerminalItem(TerminalItem.resp(null, resp))
|
||||
composeState.value = ComposeState(useLinkPreviews = false)
|
||||
|
|
|
@ -10,13 +10,13 @@ import kotlinx.coroutines.*
|
|||
expect fun ActiveCallView()
|
||||
|
||||
fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatchers.Default) {
|
||||
for (apiResp in controller.messagesChannel) {
|
||||
for (msg in controller.messagesChannel) {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call == null || call.callState > CallState.InvitationSent) break
|
||||
val msg = apiResp.resp
|
||||
if (apiResp.remoteHostId == call.remoteHostId &&
|
||||
msg is CR.ChatItemsStatusesUpdated &&
|
||||
msg.chatItems.any {
|
||||
if (msg.rhId == call.remoteHostId &&
|
||||
msg is API.Result &&
|
||||
msg.res is CR.ChatItemsStatusesUpdated &&
|
||||
msg.res.chatItems.any {
|
||||
it.chatInfo.id == call.contact.id && it.chatItem.content is CIContent.SndCall && it.chatItem.meta.itemStatus is CIStatus.SndRcvd
|
||||
}
|
||||
) {
|
||||
|
|
|
@ -554,11 +554,11 @@ fun ChatView(
|
|||
ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
|
||||
LaunchedEffect(cItem.id) {
|
||||
withContext(Dispatchers.Default) {
|
||||
for (apiResp in controller.messagesChannel) {
|
||||
val msg = apiResp.resp
|
||||
if (apiResp.remoteHostId == chatRh &&
|
||||
msg is CR.ChatItemsStatusesUpdated &&
|
||||
msg.chatItems.any { it.chatItem.id == cItem.id }
|
||||
for (msg in controller.messagesChannel) {
|
||||
if (msg.rhId == chatRh &&
|
||||
msg is API.Result &&
|
||||
msg.res is CR.ChatItemsStatusesUpdated &&
|
||||
msg.res.chatItems.any { it.chatItem.id == cItem.id }
|
||||
) {
|
||||
ciInfo = loadChatItemInfo() ?: return@withContext
|
||||
initialCiInfo = ciInfo
|
||||
|
|
|
@ -435,7 +435,7 @@ suspend fun encryptDatabase(
|
|||
}
|
||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||
appPrefs.encryptionStartedAt.set(null)
|
||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
val sqliteError = ((error as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
when {
|
||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
|
@ -449,7 +449,7 @@ suspend fun encryptDatabase(
|
|||
error != null -> {
|
||||
operationEnded(m, progressIndicator) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
|
||||
"failed to set storage encryption: ${error.responseType} ${error.details}"
|
||||
"failed to set storage encryption: error ${error.string}"
|
||||
)
|
||||
}
|
||||
false
|
||||
|
|
|
@ -468,12 +468,12 @@ private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(db
|
|||
val error = controller.testStorageEncryption(dbKey)
|
||||
if (error == null) {
|
||||
state = MigrationFromState.UploadConfirmation
|
||||
} else if (((error.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) {
|
||||
} else if (((error as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) {
|
||||
showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase(""))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.error),
|
||||
text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.details
|
||||
text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.string
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -556,11 +556,12 @@ private fun MutableState<MigrationFromState>.startUploading(
|
|||
) {
|
||||
withBGApi {
|
||||
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
|
||||
when (msg) {
|
||||
val r = msg.result
|
||||
when (r) {
|
||||
is CR.SndFileProgressXFTP -> {
|
||||
val s = state
|
||||
if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) {
|
||||
state = MigrationFromState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user)
|
||||
state = MigrationFromState.UploadProgress(r.sentSize, r.totalSize, r.fileTransferMeta.fileId, archivePath, ctrl, user)
|
||||
}
|
||||
}
|
||||
is CR.SndFileRedirectStartXFTP -> {
|
||||
|
@ -578,7 +579,7 @@ private fun MutableState<MigrationFromState>.startUploading(
|
|||
requiredHostMode = cfg.requiredHostMode
|
||||
)
|
||||
)
|
||||
state = MigrationFromState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
|
||||
state = MigrationFromState.LinkShown(r.fileTransferMeta.fileId, data.addToLink(r.rcvURIs[0]), ctrl)
|
||||
}
|
||||
is CR.SndFileError -> {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
|
@ -692,7 +693,7 @@ private class MigrationFromChatReceiver(
|
|||
val ctrl: ChatCtrl,
|
||||
val databaseUrl: File,
|
||||
var receiveMessages: Boolean = true,
|
||||
val processReceivedMsg: suspend (CR) -> Unit
|
||||
val processReceivedMsg: suspend (API) -> Unit
|
||||
) {
|
||||
fun start() {
|
||||
Log.d(TAG, "MigrationChatReceiver startReceiver")
|
||||
|
@ -701,19 +702,18 @@ private class MigrationFromChatReceiver(
|
|||
try {
|
||||
val msg = ChatController.recvMsg(ctrl)
|
||||
if (msg != null && receiveMessages) {
|
||||
val r = msg.resp
|
||||
val rhId = msg.remoteHostId
|
||||
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||
val rhId = msg.rhId
|
||||
Log.d(TAG, "processReceivedMsg: ${msg.responseType}")
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, msg))
|
||||
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
|
||||
processReceivedMsg(r)
|
||||
processReceivedMsg(msg)
|
||||
}
|
||||
if (finishedWithoutTimeout == null) {
|
||||
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
|
||||
Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType)
|
||||
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.possible_slow_function_title),
|
||||
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
|
||||
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()),
|
||||
shareText = true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -580,12 +580,13 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
|||
) {
|
||||
withBGApi {
|
||||
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
|
||||
when (msg) {
|
||||
is CR.RcvFileProgressXFTP -> {
|
||||
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
|
||||
val r = msg.result
|
||||
when {
|
||||
r is CR.RcvFileProgressXFTP -> {
|
||||
state = MigrationToState.DownloadProgress(r.receivedSize, r.totalSize, r.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy))
|
||||
}
|
||||
is CR.RcvStandaloneFileComplete -> {
|
||||
r is CR.RcvStandaloneFileComplete -> {
|
||||
delay(500)
|
||||
// User closed the whole screen before new state was saved
|
||||
if (state == null) {
|
||||
|
@ -595,22 +596,22 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
|||
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy))
|
||||
}
|
||||
}
|
||||
is CR.RcvFileError -> {
|
||||
r is CR.RcvFileError -> {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||
)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
}
|
||||
is CR.ChatRespError -> {
|
||||
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
|
||||
msg is API.Error -> {
|
||||
if (msg.err is ChatError.ChatErrorChat && msg.err.errorType is ChatErrorType.NoRcvFileUser) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||
)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
} else {
|
||||
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.chatError)}")
|
||||
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.err)}")
|
||||
}
|
||||
}
|
||||
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
|
||||
|
@ -739,7 +740,7 @@ private class MigrationToChatReceiver(
|
|||
val ctrl: ChatCtrl,
|
||||
val databaseUrl: File,
|
||||
var receiveMessages: Boolean = true,
|
||||
val processReceivedMsg: suspend (CR) -> Unit
|
||||
val processReceivedMsg: suspend (API) -> Unit
|
||||
) {
|
||||
fun start() {
|
||||
Log.d(TAG, "MigrationChatReceiver startReceiver")
|
||||
|
@ -748,19 +749,18 @@ private class MigrationToChatReceiver(
|
|||
try {
|
||||
val msg = ChatController.recvMsg(ctrl)
|
||||
if (msg != null && receiveMessages) {
|
||||
val r = msg.resp
|
||||
val rhId = msg.remoteHostId
|
||||
Log.d(TAG, "processReceivedMsg: ${r.responseType}")
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
|
||||
val rhId = msg.rhId
|
||||
Log.d(TAG, "processReceivedMsg: ${msg.responseType}")
|
||||
chatModel.addTerminalItem(TerminalItem.resp(rhId, msg))
|
||||
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
|
||||
processReceivedMsg(r)
|
||||
processReceivedMsg(msg)
|
||||
}
|
||||
if (finishedWithoutTimeout == null) {
|
||||
Log.e(TAG, "Timeout reached while processing received message: " + msg.resp.responseType)
|
||||
Log.e(TAG, "Timeout reached while processing received message: " + msg.responseType)
|
||||
if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.possible_slow_function_title),
|
||||
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.resp.responseType + "\n" + Exception().stackTraceToString()),
|
||||
text = generalGetString(MR.strings.possible_slow_function_desc).format(60, msg.responseType + "\n" + Exception().stackTraceToString()),
|
||||
shareText = true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -492,7 +492,7 @@ private suspend fun connectDesktopAddress(sessionAddress: MutableState<String>,
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, CR.ChatCmdError?>): Boolean {
|
||||
private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect: suspend () -> Pair<SomeRemoteCtrl?, ChatError?>): Boolean {
|
||||
val res = connect()
|
||||
if (res.first != null) {
|
||||
val (rc_, ctrlAppInfo, v) = res.first!!
|
||||
|
@ -505,13 +505,13 @@ private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect
|
|||
} else {
|
||||
val e = res.second ?: return false
|
||||
when {
|
||||
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
|
||||
e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
|
||||
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion)
|
||||
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
|
||||
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
|
||||
e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
|
||||
e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
|
||||
e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.remoteCtrlError.appVersion)
|
||||
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null)
|
||||
e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
|
||||
else -> {
|
||||
val errMsg = "${e.responseType}: ${e.details}"
|
||||
val errMsg = "error: ${e.string}"
|
||||
Log.e(TAG, "bad response: $errMsg")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), errMsg)
|
||||
}
|
||||
|
|
|
@ -291,7 +291,7 @@ private fun DeliveryReceiptsSection(
|
|||
SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) {
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) {
|
||||
DefaultSwitch(
|
||||
checked = currentUser.sendRcptsContacts ?: false,
|
||||
checked = currentUser.sendRcptsContacts,
|
||||
onCheckedChange = { enable ->
|
||||
setOrAskSendReceiptsContacts(enable)
|
||||
}
|
||||
|
@ -299,7 +299,7 @@ private fun DeliveryReceiptsSection(
|
|||
}
|
||||
SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) {
|
||||
DefaultSwitch(
|
||||
checked = currentUser.sendRcptsSmallGroups ?: false,
|
||||
checked = currentUser.sendRcptsSmallGroups,
|
||||
onCheckedChange = { enable ->
|
||||
setOrAskSendReceiptsGroups(enable)
|
||||
}
|
||||
|
|
|
@ -45,10 +45,10 @@ mySquaringBot _user cc = do
|
|||
race_ (forever $ void getLine) . forever $ do
|
||||
(_, evt) <- atomically . readTBQueue $ outputQ cc
|
||||
case evt of
|
||||
CEvtContactConnected _ contact _ -> do
|
||||
Right (CEvtContactConnected _ contact _) -> do
|
||||
contactConnected contact
|
||||
sendMessage cc contact welcomeMessage
|
||||
CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
|
||||
Right CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) : _} -> do
|
||||
let msg = ciContentToText mc
|
||||
number_ = readMaybe (T.unpack msg) :: Maybe Integer
|
||||
sendMessage cc contact $ case number_ of
|
||||
|
|
|
@ -39,17 +39,17 @@ broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _u
|
|||
race_ (forever $ void getLine) . forever $ do
|
||||
(_, evt) <- atomically . readTBQueue $ outputQ cc
|
||||
case evt of
|
||||
CEvtContactConnected _ ct _ -> do
|
||||
Right (CEvtContactConnected _ ct _) -> do
|
||||
contactConnected ct
|
||||
sendMessage cc ct welcomeMessage
|
||||
CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) : _}
|
||||
Right CEvtNewChatItems {chatItems = (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) : _}
|
||||
| sender `notElem` publishers -> do
|
||||
sendReply prohibitedMessage
|
||||
deleteMessage cc ct $ chatItemId' ci
|
||||
| allowContent mc ->
|
||||
void $ forkIO $
|
||||
sendChatCmd cc (SendMessageBroadcast mc) >>= \case
|
||||
CRBroadcastSent {successes, failures} ->
|
||||
Right CRBroadcastSent {successes, failures} ->
|
||||
sendReply $ "Forwarded to " <> tshow successes <> " contact(s), " <> tshow failures <> " errors"
|
||||
r -> putStrLn $ "Error broadcasting message: " <> show r
|
||||
| otherwise ->
|
||||
|
|
|
@ -2,20 +2,23 @@
|
|||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
|
||||
module Server where
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Except
|
||||
import Control.Monad.Reader
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import Data.Aeson (FromJSON, ToJSON (..))
|
||||
import qualified Data.Aeson as J
|
||||
import qualified Data.Aeson.TH as JQ
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import GHC.Generics (Generic)
|
||||
|
@ -26,7 +29,7 @@ import Simplex.Chat.Controller
|
|||
import Simplex.Chat.Core
|
||||
import Simplex.Chat.Library.Commands
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Messaging.Parsers (defaultJSON)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON)
|
||||
import Simplex.Messaging.Transport.Server (runLocalTCPServer)
|
||||
import Simplex.Messaging.Util (raceAny_)
|
||||
import UnliftIO.Exception
|
||||
|
@ -35,13 +38,32 @@ import UnliftIO.STM
|
|||
data ChatSrvRequest = ChatSrvRequest {corrId :: Text, cmd :: Text}
|
||||
deriving (Generic, FromJSON)
|
||||
|
||||
data ChatSrvResponse r = ChatSrvResponse {corrId :: Maybe Text, resp :: r}
|
||||
data ChatSrvResponse r = ChatSrvResponse {corrId :: Maybe Text, resp :: CSRBody r}
|
||||
|
||||
data CSRBody r = CSRBody {csrBody :: Either ChatError r}
|
||||
|
||||
-- backwards compatible encoding, to avoid breaking any chat bots
|
||||
data ObjChatCmdError = ObjChatCmdError {chatError :: ChatError}
|
||||
|
||||
data ObjChatError = ObjChatError {chatError :: ChatError}
|
||||
|
||||
$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "Obj") ''ObjChatCmdError)
|
||||
|
||||
$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "Obj") ''ObjChatError)
|
||||
|
||||
instance ToJSON (CSRBody ChatResponse) where
|
||||
toJSON = toJSON . first ObjChatCmdError . csrBody
|
||||
toEncoding = toEncoding . first ObjChatCmdError . csrBody
|
||||
|
||||
instance ToJSON (CSRBody ChatEvent) where
|
||||
toJSON = toJSON . first ObjChatError . csrBody
|
||||
toEncoding = toEncoding . first ObjChatError . csrBody
|
||||
|
||||
data AChatSrvResponse = forall r. ToJSON (ChatSrvResponse r) => ACR (ChatSrvResponse r)
|
||||
|
||||
$(pure [])
|
||||
|
||||
instance ToJSON r => ToJSON (ChatSrvResponse r) where
|
||||
instance ToJSON (CSRBody r) => ToJSON (ChatSrvResponse r) where
|
||||
toEncoding = $(JQ.mkToEncoding defaultJSON ''ChatSrvResponse)
|
||||
toJSON = $(JQ.mkToJSON defaultJSON ''ChatSrvResponse)
|
||||
|
||||
|
@ -91,8 +113,8 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do
|
|||
>>= processCommand
|
||||
>>= atomically . writeTBQueue sndQ . ACR
|
||||
output ChatClient {sndQ} = forever $ do
|
||||
(_, resp) <- atomically . readTBQueue $ outputQ cc
|
||||
atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId = Nothing, resp}
|
||||
(_, r) <- atomically . readTBQueue $ outputQ cc
|
||||
atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId = Nothing, resp = CSRBody r}
|
||||
receive ws ChatClient {rcvQ, sndQ} = forever $ do
|
||||
s <- WS.receiveData ws
|
||||
case J.decodeStrict' s of
|
||||
|
@ -103,11 +125,9 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do
|
|||
Left e -> sendError (Just corrId) e
|
||||
Nothing -> sendError Nothing "invalid request"
|
||||
where
|
||||
sendError corrId e = atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId, resp = chatCmdError Nothing e}
|
||||
sendError corrId e = atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId, resp = CSRBody $ chatCmdError e}
|
||||
processCommand (corrId, cmd) =
|
||||
runReaderT (runExceptT $ processChatCommand cmd) cc >>= \case
|
||||
Right resp -> response resp
|
||||
Left e -> response $ CRChatCmdError Nothing e
|
||||
response <$> runReaderT (runExceptT $ processChatCommand cmd) cc
|
||||
where
|
||||
response resp = pure ChatSrvResponse {corrId = Just corrId, resp}
|
||||
response r = ChatSrvResponse {corrId = Just corrId, resp = CSRBody r}
|
||||
clientDisconnected _ = pure ()
|
||||
|
|
|
@ -63,8 +63,16 @@ data DirectoryEvent
|
|||
| DELogChatResponse Text
|
||||
deriving (Show)
|
||||
|
||||
crDirectoryEvent :: ChatEvent -> Maybe DirectoryEvent
|
||||
crDirectoryEvent :: Either ChatError ChatEvent -> Maybe DirectoryEvent
|
||||
crDirectoryEvent = \case
|
||||
Right evt -> crDirectoryEvent_ evt
|
||||
Left e -> case e of
|
||||
ChatErrorAgent {agentError = BROKER _ NETWORK} -> Nothing
|
||||
ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing
|
||||
_ -> Just $ DELogChatResponse $ "chat error: " <> tshow e
|
||||
|
||||
crDirectoryEvent_ :: ChatEvent -> Maybe DirectoryEvent
|
||||
crDirectoryEvent_ = \case
|
||||
CEvtContactConnected {contact} -> Just $ DEContactConnected contact
|
||||
CEvtReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole}
|
||||
CEvtUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember
|
||||
|
@ -92,10 +100,6 @@ crDirectoryEvent = \case
|
|||
ciId = chatItemId' ci
|
||||
err = ADC SDRUser DCUnknownCommand
|
||||
CEvtMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage
|
||||
CEvtChatError {chatError} -> case chatError of
|
||||
ChatErrorAgent {agentError = BROKER _ NETWORK} -> Nothing
|
||||
ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing
|
||||
_ -> Just $ DELogChatResponse $ "chat error: " <> tshow chatError
|
||||
CEvtChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors)
|
||||
_ -> Nothing
|
||||
where
|
||||
|
|
|
@ -60,7 +60,7 @@ import Simplex.Chat.Terminal (terminalChatConfig)
|
|||
import Simplex.Chat.Terminal.Main (simplexChatCLI')
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||
import Simplex.Chat.View (serializeChatError, serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..))
|
||||
import Simplex.Messaging.Agent.Store.Common (withTransaction)
|
||||
import Simplex.Messaging.Agent.Protocol (SConnectionMode (..), sameConnReqContact, sameShortLinkContact)
|
||||
|
@ -197,7 +197,7 @@ readBlockedWordsConfig DirectoryOpts {blockedFragmentsFile, blockedWordsFile, na
|
|||
unless testing $ putStrLn $ "Blocked fragments: " <> show (length blockedFragments) <> ", blocked words: " <> show (length blockedWords) <> ", spelling rules: " <> show (M.size spelling)
|
||||
pure BlockedWordsConfig {blockedFragments, blockedWords, extensionRules, spelling}
|
||||
|
||||
directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> ChatEvent -> IO ()
|
||||
directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> Either ChatError ChatEvent -> IO ()
|
||||
directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc event =
|
||||
forM_ (crDirectoryEvent event) $ \case
|
||||
DEContactConnected ct -> deContactConnected ct
|
||||
|
@ -249,7 +249,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)])
|
||||
getGroups_ search_ =
|
||||
sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case
|
||||
CRGroupsList {groups} -> pure $ Just groups
|
||||
Right CRGroupsList {groups} -> pure $ Just groups
|
||||
_ -> pure Nothing
|
||||
|
||||
getDuplicateGroup :: GroupInfo -> IO (Maybe DuplicateGroup)
|
||||
|
@ -281,7 +281,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
void $ addGroupReg st ct g GRSProposed
|
||||
r <- sendChatCmd cc $ APIJoinGroup groupId MFNone
|
||||
sendMessage cc ct $ case r of
|
||||
CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…"
|
||||
Right CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…"
|
||||
_ -> "Error joining group " <> displayName <> ", please re-send the invitation!"
|
||||
|
||||
deContactConnected :: Contact -> IO ()
|
||||
|
@ -337,7 +337,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
$>>= \mId -> resp <$> sendChatCmd cc (APIGroupMemberInfo dbGroupId mId)
|
||||
where
|
||||
resp = \case
|
||||
CRGroupMemberInfo {member} -> Just member
|
||||
Right CRGroupMemberInfo {member} -> Just member
|
||||
_ -> Nothing
|
||||
|
||||
deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO ()
|
||||
|
@ -349,7 +349,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g
|
||||
notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…"
|
||||
sendChatCmd cc (APICreateGroupLink groupId GRMember False) >>= \case
|
||||
CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> do
|
||||
Right CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> do
|
||||
setGroupStatus st gr GRSPendingUpdate
|
||||
notifyOwner
|
||||
gr
|
||||
|
@ -357,7 +357,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
\Please add it to the group welcome message.\n\
|
||||
\For example, add:"
|
||||
notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact gLink)
|
||||
CRChatCmdError _ (ChatError e) -> case e of
|
||||
Left (ChatError e) -> case e of
|
||||
CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin."
|
||||
CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group."
|
||||
CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined"
|
||||
|
@ -446,7 +446,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
|
||||
where
|
||||
profileUpdate = \case
|
||||
CRGroupLink {connLinkContact = CCLink cr sl_} ->
|
||||
Right CRGroupLink {connLinkContact = CCLink cr sl_} ->
|
||||
let hadLinkBefore = profileHasGroupLink fromGroup
|
||||
hasLinkNow = profileHasGroupLink toGroup
|
||||
profileHasGroupLink GroupInfo {groupProfile = gp} =
|
||||
|
@ -503,7 +503,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_
|
||||
gmId = groupMemberId' m
|
||||
sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case
|
||||
CRJoinedGroupMember {} -> do
|
||||
Right CRJoinedGroupMember {} -> do
|
||||
atomically $ TM.delete gmId $ pendingCaptchas env
|
||||
logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g
|
||||
r -> logError $ "unexpected accept member response: " <> tshow r
|
||||
|
@ -528,7 +528,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
let gmId = groupMemberId' m
|
||||
sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice]
|
||||
sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case
|
||||
CRUserDeletedMembers _ _ (_ : _) _ -> do
|
||||
Right (CRUserDeletedMembers _ _ (_ : _) _) -> do
|
||||
atomically $ TM.delete gmId $ pendingCaptchas env
|
||||
logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g
|
||||
r -> logError $ "unexpected remove member response: " <> tshow r
|
||||
|
@ -891,18 +891,21 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
let groupRef = groupReference' groupId gName
|
||||
withGroupAndReg sendReply groupId gName $ \_ _ ->
|
||||
sendChatCmd cc (APIGetGroupLink groupId) >>= \case
|
||||
CRGroupLink {connLinkContact = CCLink cReq _, memberRole} ->
|
||||
Right CRGroupLink {connLinkContact = CCLink cReq _, memberRole} ->
|
||||
sendReply $ T.unlines
|
||||
[ "The link to join the group " <> groupRef <> ":",
|
||||
strEncodeTxt $ simplexChatContact cReq,
|
||||
"New member role: " <> strEncodeTxt memberRole
|
||||
]
|
||||
CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) ->
|
||||
Left (ChatErrorStore (SEGroupLinkNotFound _)) ->
|
||||
sendReply $ "The group " <> groupRef <> " has no public link."
|
||||
r -> do
|
||||
Right r -> do
|
||||
ts <- getCurrentTime
|
||||
tz <- getCurrentTimeZone
|
||||
let resp = T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||
let resp = T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r
|
||||
sendReply $ "Unexpected error:\n" <> resp
|
||||
Left e -> do
|
||||
let resp = T.pack $ serializeChatError True (config cc) e
|
||||
sendReply $ "Unexpected error:\n" <> resp
|
||||
DCSendToGroupOwner groupId gName msg -> do
|
||||
let groupRef = groupReference' groupId gName
|
||||
|
@ -944,11 +947,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
inviteToOwnersGroup :: KnownGroup -> GroupReg -> (Either Text () -> IO a) -> IO a
|
||||
inviteToOwnersGroup KnownGroup {groupId = ogId} GroupReg {dbContactId = ctId} cont =
|
||||
sendChatCmd cc (APIListMembers ogId) >>= \case
|
||||
CRGroupMembers _ (Group _ ms)
|
||||
Right (CRGroupMembers _ (Group _ ms))
|
||||
| alreadyMember ms -> cont $ Left "Owner is already a member of owners' group"
|
||||
| otherwise -> do
|
||||
sendChatCmd cc (APIAddMember ogId ctId GRMember) >>= \case
|
||||
CRSentGroupInvitation {} -> do
|
||||
Right CRSentGroupInvitation {} -> do
|
||||
printLog cc CLLInfo $ "invited contact ID " <> show ctId <> " to owners' group"
|
||||
cont $ Right ()
|
||||
r -> contErr r
|
||||
|
@ -969,10 +972,13 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
|
|||
deSuperUserCommand ct ciId cmd
|
||||
| knownContact ct `elem` superUsers = case cmd of
|
||||
DCExecuteCommand cmdStr ->
|
||||
sendChatCmdStr cc cmdStr >>= \r -> do
|
||||
ts <- getCurrentTime
|
||||
tz <- getCurrentTimeZone
|
||||
sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||
sendChatCmdStr cc cmdStr >>= \case
|
||||
Right r -> do
|
||||
ts <- getCurrentTime
|
||||
tz <- getCurrentTimeZone
|
||||
sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) (config cc) ts tz Nothing r
|
||||
Left e ->
|
||||
sendReply $ T.pack $ serializeChatError True (config cc) e
|
||||
DCCommandError tag -> sendReply $ "Command error: " <> tshow tag
|
||||
| otherwise = sendReply "You are not allowed to use this command"
|
||||
where
|
||||
|
@ -1045,7 +1051,7 @@ setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe
|
|||
setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole)
|
||||
where
|
||||
resp = \case
|
||||
CRGroupLink _ _ (CCLink gLink _) _ -> Just gLink
|
||||
Right (CRGroupLink _ _ (CCLink gLink _) _) -> Just gLink
|
||||
_ -> Nothing
|
||||
|
||||
unexpectedError :: Text -> Text
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue