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:
Evgeny 2025-05-05 11:51:22 +01:00 committed by GitHub
parent a0d1cca389
commit 24b0f0290b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 2131 additions and 2177 deletions

View file

@ -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))")

View file

@ -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 {

View file

@ -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

View file

@ -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 &&

View file

@ -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)!)
}
}

View file

@ -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 {

View file

@ -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))"))

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
}
}

View file

@ -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()

View file

@ -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
}
}
}
}

View file

@ -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 {

View file

@ -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
}
}
}
}

View file

@ -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
}

View file

@ -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")
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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]
}
}

View file

@ -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)):

View file

@ -229,5 +229,5 @@ fun isMediaIntent(intent: Intent): Boolean =
// val str: String = """
// """.trimIndent()
//
// println(json.decodeFromString<APIResponse>(str))
// println(json.decodeFromString<APIResult>(str))
//}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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
}
) {

View file

@ -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

View file

@ -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

View file

@ -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
)
}

View file

@ -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
)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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 ->

View file

@ -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 ()

View file

@ -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

View file

@ -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