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) try await apiVerifyToken(token: token, nonce: nonce, code: verification)
m.tokenStatus = .active m.tokenStatus = .active
} catch { } 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 m.tokenStatus = .expired
} }
logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))") logger.error("AppDelegate: didReceiveRemoteNotification: apiVerifyToken or apiIntervalNofication error: \(responseError(error))")

View file

@ -580,8 +580,8 @@ enum ChatCommand: ChatCmdProtocol {
} }
} }
enum ChatResponse: Decodable, Error, ChatRespProtocol { // ChatResponse is split to three enums to reduce stack size used when parsing it, parsing large enums is very inefficient.
case response(type: String, json: String) enum ChatResponse0: Decodable, ChatAPIResult {
case activeUser(user: User) case activeUser(user: User)
case usersList(users: [UserInfo]) case usersList(users: [UserInfo])
case chatStarted case chatStarted
@ -611,205 +611,43 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64]) case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64])
case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection)
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef)
case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan)
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
case sentInvitation(user: UserRef, connection: PendingContactConnection)
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
case contactAlreadyExists(user: UserRef, contact: Contact)
case contactDeleted(user: UserRef, contact: Contact)
case chatCleared(user: UserRef, chatInfo: ChatInfo)
case userProfileNoChange(user: User)
case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary)
case userPrivacy(user: User, updatedUser: User)
case contactAliasUpdated(user: UserRef, toContact: Contact)
case groupAliasUpdated(user: UserRef, toGroup: GroupInfo)
case connectionAliasUpdated(user: UserRef, toConnection: PendingContactConnection)
case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact)
case userContactLink(user: User, contactLink: UserContactLink)
case userContactLinkUpdated(user: User, contactLink: UserContactLink)
case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink)
case userContactLinkDeleted(user: User)
case acceptingContactRequest(user: UserRef, contact: Contact)
case contactRequestRejected(user: UserRef)
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case newChatItems(user: UserRef, chatItems: [AChatItem])
case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set<Int64>, byUser: Bool, member_: GroupMember?)
case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?)
case chatItemUpdated(user: UserRef, chatItem: AChatItem)
case chatItemNotChanged(user: UserRef, chatItem: AChatItem)
case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction)
case reactionMembers(user: UserRef, memberReactions: [MemberReaction])
case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool)
case contactsList(user: UserRef, contacts: [Contact])
// group responses
case groupCreated(user: UserRef, groupInfo: GroupInfo)
case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember)
case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?)
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)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLink(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo)
case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
// receiving file responses
case rcvFileAccepted(user: UserRef, chatItem: AChatItem)
case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer)
case standaloneFileInfo(fileMeta: MigrationFileLinkData?)
case rcvStandaloneFileCreated(user: UserRef, rcvFileTransfer: RcvFileTransfer)
case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
// sending file responses
case sndFileCancelled(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case sndStandaloneFileCreated(user: UserRef, fileTransferMeta: FileTransferMeta) // returned by _upload
case sndFileStartXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) // not used
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
// call invitations
case callInvitations(callInvitations: [RcvCallInvitation])
// notifications
case ntfTokenStatus(status: NtfTknStatus)
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)
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
// misc
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
case cmdOk(user_: UserRef?)
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 { var responseType: String {
get {
switch self { switch self {
case let .response(type, _): return "* \(type)" case .activeUser: "activeUser"
case .activeUser: return "activeUser" case .usersList: "usersList"
case .usersList: return "usersList" case .chatStarted: "chatStarted"
case .chatStarted: return "chatStarted" case .chatRunning: "chatRunning"
case .chatRunning: return "chatRunning" case .chatStopped: "chatStopped"
case .chatStopped: return "chatStopped" case .apiChats: "apiChats"
case .apiChats: return "apiChats" case .apiChat: "apiChat"
case .apiChat: return "apiChat" case .chatTags: "chatTags"
case .chatTags: return "chatTags" case .chatItemInfo: "chatItemInfo"
case .chatItemInfo: return "chatItemInfo" case .serverTestResult: "serverTestResult"
case .serverTestResult: return "serverTestResult" case .serverOperatorConditions: "serverOperators"
case .serverOperatorConditions: return "serverOperators" case .userServers: "userServers"
case .userServers: return "userServers" case .userServersValidation: "userServersValidation"
case .userServersValidation: return "userServersValidation" case .usageConditions: "usageConditions"
case .usageConditions: return "usageConditions" case .chatItemTTL: "chatItemTTL"
case .chatItemTTL: return "chatItemTTL" case .networkConfig: "networkConfig"
case .networkConfig: return "networkConfig" case .contactInfo: "contactInfo"
case .contactInfo: return "contactInfo" case .groupMemberInfo: "groupMemberInfo"
case .groupMemberInfo: return "groupMemberInfo" case .queueInfo: "queueInfo"
case .queueInfo: return "queueInfo" case .contactSwitchStarted: "contactSwitchStarted"
case .contactSwitchStarted: return "contactSwitchStarted" case .groupMemberSwitchStarted: "groupMemberSwitchStarted"
case .groupMemberSwitchStarted: return "groupMemberSwitchStarted" case .contactSwitchAborted: "contactSwitchAborted"
case .contactSwitchAborted: return "contactSwitchAborted" case .groupMemberSwitchAborted: "groupMemberSwitchAborted"
case .groupMemberSwitchAborted: return "groupMemberSwitchAborted" case .contactRatchetSyncStarted: "contactRatchetSyncStarted"
case .contactRatchetSyncStarted: return "contactRatchetSyncStarted" case .groupMemberRatchetSyncStarted: "groupMemberRatchetSyncStarted"
case .groupMemberRatchetSyncStarted: return "groupMemberRatchetSyncStarted" case .contactCode: "contactCode"
case .contactCode: return "contactCode" case .groupMemberCode: "groupMemberCode"
case .groupMemberCode: return "groupMemberCode" case .connectionVerified: "connectionVerified"
case .connectionVerified: return "connectionVerified" case .tagsUpdated: "tagsUpdated"
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"
}
} }
} }
var details: String { var details: String {
get {
switch self { switch self {
case let .response(_, json): return json
case let .activeUser(user): return String(describing: user) case let .activeUser(user): return String(describing: user)
case let .usersList(users): return String(describing: users) case let .usersList(users): return String(describing: users)
case .chatStarted: return noDetails case .chatStarted: return noDetails
@ -841,15 +679,108 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\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 .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 .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))") static func fallbackResult(_ type: String, _ json: NSDictionary) -> ChatResponse0? {
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) if type == "apiChats" {
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) if let r = parseApiChats(json) {
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact)) return .apiChats(user: r.user, chats: r.chats)
case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact)) }
} 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)
case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan)
case sentConfirmation(user: UserRef, connection: PendingContactConnection)
case sentInvitation(user: UserRef, connection: PendingContactConnection)
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)
case userPrivacy(user: User, updatedUser: User)
case contactAliasUpdated(user: UserRef, toContact: Contact)
case groupAliasUpdated(user: UserRef, toGroup: GroupInfo)
case connectionAliasUpdated(user: UserRef, toConnection: PendingContactConnection)
case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact)
case userContactLink(user: User, contactLink: UserContactLink)
case userContactLinkUpdated(user: User, contactLink: UserContactLink)
case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink)
case userContactLinkDeleted(user: User)
case acceptingContactRequest(user: UserRef, contact: Contact)
case contactRequestRejected(user: UserRef)
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case newChatItems(user: UserRef, chatItems: [AChatItem])
case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set<Int64>, byUser: Bool, member_: GroupMember?)
case forwardPlan(user: UserRef, chatItemIds: [Int64], forwardConfirmation: ForwardConfirmation?)
case chatItemUpdated(user: UserRef, chatItem: AChatItem)
case chatItemNotChanged(user: UserRef, chatItem: AChatItem)
case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction)
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 .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 let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo))
case .userProfileNoChange: return noDetails case .userProfileNoChange: return noDetails
case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile)) case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile))
@ -880,13 +811,118 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
"deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n")
return withUser(u, itemsString + "\nbyUser: \(byUser)") return withUser(u, itemsString + "\nbyUser: \(byUser)")
case let .contactsList(u, contacts): return withUser(u, String(describing: contacts)) 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)
case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?)
case userDeletedMembers(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], withMessages: Bool)
case leftMemberUser(user: UserRef, groupInfo: GroupInfo)
case groupMembers(user: UserRef, group: SimpleXChat.Group)
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)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLink(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo)
case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
// receiving file responses
case rcvFileAccepted(user: UserRef, chatItem: AChatItem)
case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer)
case standaloneFileInfo(fileMeta: MigrationFileLinkData?)
case rcvStandaloneFileCreated(user: UserRef, rcvFileTransfer: RcvFileTransfer)
case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
// sending file responses
case sndFileCancelled(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case sndStandaloneFileCreated(user: UserRef, fileTransferMeta: FileTransferMeta) // returned by _upload
case sndFileStartXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) // not used
case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
// call invitations
case callInvitations(callInvitations: [RcvCallInvitation])
// notifications
case ntfTokenStatus(status: NtfTknStatus)
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String)
case ntfConns(ntfConns: [NtfConn])
case connNtfMessages(receivedMsgs: [NtfMsgInfo?])
// remote desktop responses
case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo])
case remoteCtrlConnecting(remoteCtrl_: RemoteCtrlInfo?, ctrlAppInfo: CtrlAppInfo, appVersion: String)
case remoteCtrlConnected(remoteCtrl: RemoteCtrlInfo)
// misc
case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration])
case cmdOk(user_: UserRef?)
case agentSubsTotal(user: UserRef, subsTotal: SMPServerSubs, hasSession: Bool)
case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary)
case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs)
case archiveExported(archiveErrors: [ArchiveError])
case archiveImported(archiveErrors: [ArchiveError])
case appSettings(appSettings: AppSettings)
var responseType: String {
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 {
switch self {
case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) 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 .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 .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 .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 .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .groupMembers(u, group): return withUser(u, String(describing: group)) 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 .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 .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 .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
@ -909,7 +945,6 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
case let .ntfToken(token, status, ntfMode, ntfServer): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)\nntfServer: \(ntfServer)" 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 .ntfConns(ntfConns): return String(describing: ntfConns)
case let .connNtfMessages(receivedMsgs): return "receivedMsgs: \(String(describing: receivedMsgs))" 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 .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 .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 .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
@ -918,7 +953,6 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
case let .agentSubsTotal(u, subsTotal, hasSession): return withUser(u, "subsTotal: \(String(describing: subsTotal))\nhasSession: \(hasSession)") 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 .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary))
case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary)) 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 .archiveExported(archiveErrors): return String(describing: archiveErrors)
case let .archiveImported(archiveErrors): return String(describing: archiveErrors) case let .archiveImported(archiveErrors): return String(describing: archiveErrors)
case let .appSettings(appSettings): return String(describing: appSettings) case let .appSettings(appSettings): return String(describing: appSettings)
@ -926,69 +960,7 @@ enum ChatResponse: Decodable, Error, ChatRespProtocol {
} }
} }
private var noDetails: String { get { "\(responseType): no details" } } enum ChatEvent: Decodable, ChatAPIResult {
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
}
}
}
enum ChatEvent: Decodable, ChatEventProtocol {
case event(type: String, json: String)
case chatSuspended case chatSuspended
case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress) case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress)
case groupMemberSwitch(user: UserRef, groupInfo: GroupInfo, member: GroupMember, 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) case remoteCtrlStopped(rcsState: RemoteCtrlSessionState, rcStopReason: RemoteCtrlStopReason)
// pq // pq
case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool) case contactPQEnabled(user: UserRef, contact: Contact, pqEnabled: Bool)
case chatError(user_: UserRef?, chatError: ChatError)
var eventType: String { var responseType: String {
switch self { switch self {
case let .event(type, _): "* \(type)"
case .chatSuspended: "chatSuspended" case .chatSuspended: "chatSuspended"
case .contactSwitch: "contactSwitch" case .contactSwitch: "contactSwitch"
case .groupMemberSwitch: "groupMemberSwitch" case .groupMemberSwitch: "groupMemberSwitch"
@ -1135,13 +1105,11 @@ enum ChatEvent: Decodable, ChatEventProtocol {
case .remoteCtrlConnected: "remoteCtrlConnected" case .remoteCtrlConnected: "remoteCtrlConnected"
case .remoteCtrlStopped: "remoteCtrlStopped" case .remoteCtrlStopped: "remoteCtrlStopped"
case .contactPQEnabled: "contactPQEnabled" case .contactPQEnabled: "contactPQEnabled"
case .chatError: "chatError"
} }
} }
var details: String { var details: String {
switch self { switch self {
case let .event(_, json): return json
case .chatSuspended: return noDetails case .chatSuspended: return noDetails
case let .contactSwitch(u, contact, switchProgress): return withUser(u, "contact: \(String(describing: contact))\nswitchProgress: \(String(describing: switchProgress))") 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))") 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 .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl)
case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))" 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 .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 { 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(.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 ChatReceiver.shared.messagesChannel = nil
return return
} }
if case let .chatItemsStatusesUpdated(_, chatItems) = msg, if case let .result(.chatItemsStatusesUpdated(_, chatItems)) = msg,
chatItems.contains(where: { ci in chatItems.contains(where: { ci in
ci.chatInfo.id == call.contact.id && ci.chatInfo.id == call.contact.id &&
ci.chatItem.content.isSndCall && ci.chatItem.content.isSndCall &&

View file

@ -7,10 +7,11 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat
struct CIInvalidJSONView: View { struct CIInvalidJSONView: View {
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
var json: String var json: Data?
@State private var showJSON = false @State private var showJSON = false
var body: some View { var body: some View {
@ -25,7 +26,7 @@ struct CIInvalidJSONView: View {
.textSelection(.disabled) .textSelection(.disabled)
.onTapGesture { showJSON = true } .onTapGesture { showJSON = true }
.appSheet(isPresented: $showJSON) { .appSheet(isPresented: $showJSON) {
invalidJSONView(json) invalidJSONView(dataToString(json))
} }
} }
} }
@ -49,6 +50,6 @@ func invalidJSONView(_ json: String) -> some View {
struct CIInvalidJSONView_Previews: PreviewProvider { struct CIInvalidJSONView_Previews: PreviewProvider {
static var previews: some View { 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") Text("invalid chat data")
.foregroundColor(.red) .foregroundColor(.red)
.padding(4) .padding(4)
.frame(height: dynamicRowHeight) .frame(height: dynamicRowHeight)
.onTapGesture { showInvalidJSON = true } .onTapGesture { showInvalidJSON = true }
.appSheet(isPresented: $showInvalidJSON) { .appSheet(isPresented: $showInvalidJSON) {
invalidJSONView(json) invalidJSONView(dataToString(json))
.environment(\EnvironmentValues.refresh as! WritableKeyPath<EnvironmentValues, RefreshAction?>, nil) .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 { func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert {
if let r = error as? ChatResponse, if let r = error as? ChatError,
let alert = getNetworkErrorAlert(r) { let alert = getNetworkErrorAlert(r) {
return alert return alert
} else { } else {

View file

@ -173,7 +173,7 @@ struct DatabaseEncryptionView: View {
} }
return true return true
} catch let error { } catch let error {
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse { if case .errorDatabase(.errorExport(.errorNotADatabase)) = error as? ChatError {
await operationEnded(.currentPassphraseError) await operationEnded(.currentPassphraseError)
} else { } else {
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))")) 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 chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
await MainActor.run { await MainActor.run {
switch msg { switch msg {
case let .sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize): case let .result(.sndFileProgressXFTP(_, _, fileTransferMeta, sentSize, totalSize)):
if case let .uploadProgress(uploaded, total, _, _, _) = migrationState, uploaded != total { if case let .uploadProgress(uploaded, total, _, _, _) = migrationState, uploaded != total {
migrationState = .uploadProgress(uploadedBytes: sentSize, totalBytes: totalSize, fileId: fileTransferMeta.fileId, archivePath: archivePath, ctrl: ctrl) 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) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
migrationState = .linkCreation migrationState = .linkCreation
} }
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs): case let .result(.sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs)):
let cfg = getNetCfg() let cfg = getNetCfg()
let proxy: NetworkProxy? = if cfg.socksProxy == nil { let proxy: NetworkProxy? = if cfg.socksProxy == nil {
nil nil
@ -546,11 +546,11 @@ struct MigrateFromDevice: View {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
migrationState = .linkShown(fileId: fileTransferMeta.fileId, link: data.addToLink(link: rcvURIs[0]), archivePath: archivePath, ctrl: ctrl) 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") alert = .error(title: "Upload failed", error: "Check your internet connection and try again")
migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath) migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
default: default:
logger.debug("unsupported event: \(msg.eventType)") logger.debug("unsupported event: \(msg.responseType)")
} }
} }
} }
@ -691,7 +691,7 @@ private struct PassphraseConfirmationView: View {
migrationState = .uploadConfirmation migrationState = .uploadConfirmation
} }
} catch let error { } catch let error {
if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse { if case .errorDatabase(.errorOpen(.errorNotADatabase)) = error as? ChatError {
showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert) showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert)
} else { } else {
alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(responseError(error))) alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(responseError(error)))
@ -733,11 +733,11 @@ func chatStoppedView() -> some View {
private class MigrationChatReceiver { private class MigrationChatReceiver {
let ctrl: chat_ctrl let ctrl: chat_ctrl
let databaseUrl: URL let databaseUrl: URL
let processReceivedMsg: (ChatEvent) async -> Void let processReceivedMsg: (APIResult<ChatEvent>) async -> Void
private var receiveLoop: Task<Void, Never>? private var receiveLoop: Task<Void, Never>?
private var receiveMessages = true 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.ctrl = ctrl
self.databaseUrl = databaseUrl self.databaseUrl = databaseUrl
self.processReceivedMsg = processReceivedMsg self.processReceivedMsg = processReceivedMsg
@ -752,11 +752,11 @@ private class MigrationChatReceiver {
func receiveMsgLoop() async { func receiveMsgLoop() async {
// TODO use function that has timeout // TODO use function that has timeout
if let msg: ChatEvent = await chatRecvMsg(ctrl) { if let msg: APIResult<ChatEvent> = await chatRecvMsg(ctrl) {
Task { 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) await processReceivedMsg(msg)
} }
if self.receiveMessages { if self.receiveMessages {

View file

@ -496,10 +496,10 @@ struct MigrateToDevice: View {
chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in chatReceiver = MigrationChatReceiver(ctrl: ctrl, databaseUrl: tempDatabaseUrl) { msg in
await MainActor.run { await MainActor.run {
switch msg { 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) 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)) MigrationToDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
case .rcvStandaloneFileComplete: case .result(.rcvStandaloneFileComplete):
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// User closed the whole screen before new state was saved // User closed the whole screen before new state was saved
if migrationState == nil { if migrationState == nil {
@ -509,14 +509,14 @@ struct MigrateToDevice: View {
MigrationToDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent)) 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") alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) 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") alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath) migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
default: 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 { private class MigrationChatReceiver {
let ctrl: chat_ctrl let ctrl: chat_ctrl
let databaseUrl: URL let databaseUrl: URL
let processReceivedMsg: (ChatEvent) async -> Void let processReceivedMsg: (APIResult<ChatEvent>) async -> Void
private var receiveLoop: Task<Void, Never>? private var receiveLoop: Task<Void, Never>?
private var receiveMessages = true 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.ctrl = ctrl
self.databaseUrl = databaseUrl self.databaseUrl = databaseUrl
self.processReceivedMsg = processReceivedMsg self.processReceivedMsg = processReceivedMsg
@ -772,9 +772,9 @@ private class MigrationChatReceiver {
// TODO use function that has timeout // TODO use function that has timeout
if let msg = await chatRecvMsg(ctrl) { if let msg = await chatRecvMsg(ctrl) {
Task { 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) await processReceivedMsg(msg)
} }
if self.receiveMessages { if self.receiveMessages {

View file

@ -236,15 +236,15 @@ private func showCreateProfileAlert(
_ error: Error _ error: Error
) { ) {
let m = ChatModel.shared let m = ChatModel.shared
switch error as? ChatResponse { switch error as? ChatError {
case .chatCmdError(_, .errorStore(.duplicateName)), case .errorStore(.duplicateName),
.chatCmdError(_, .error(.userExists)): .error(.userExists):
if m.currentUser == nil { if m.currentUser == nil {
AlertManager.shared.showAlert(duplicateUserAlert) AlertManager.shared.showAlert(duplicateUserAlert)
} else { } else {
showAlert(.duplicateUserError) showAlert(.duplicateUserError)
} }
case .chatCmdError(_, .error(.invalidDisplayName)): case .error(.invalidDisplayName):
if m.currentUser == nil { if m.currentUser == nil {
AlertManager.shared.showAlert(invalidDisplayNameAlert) AlertManager.shared.showAlert(invalidDisplayNameAlert)
} else { } else {

View file

@ -456,12 +456,12 @@ struct ConnectDesktopView: View {
} }
} catch let e { } catch let e {
await MainActor.run { await MainActor.run {
switch e as? ChatResponse { switch e as? ChatError {
case .chatCmdError(_, .errorRemoteCtrl(.badInvitation)): alert = .badInvitationError case .errorRemoteCtrl(.badInvitation): alert = .badInvitationError
case .chatCmdError(_, .error(.commandError)): alert = .badInvitationError case .error(.commandError): alert = .badInvitationError
case let .chatCmdError(_, .errorRemoteCtrl(.badVersion(v))): alert = .badVersionError(version: v) case let .errorRemoteCtrl(.badVersion(v)): alert = .badVersionError(version: v)
case .chatCmdError(_, .errorAgent(.RCP(.version))): alert = .badVersionError(version: nil) case .errorAgent(.RCP(.version)): alert = .badVersionError(version: nil)
case .chatCmdError(_, .errorAgent(.RCP(.ctrlAuth))): alert = .desktopDisconnectedError case .errorAgent(.RCP(.ctrlAuth)): alert = .desktopDisconnectedError
default: errorAlert(e) default: errorAlert(e)
} }
} }

View file

@ -145,18 +145,18 @@ struct TerminalView: View {
} }
func consoleSendMessage() { func consoleSendMessage() {
let cmd = ChatCommand.string(composeState.message)
if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) { 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 { Task {
await TerminalItems.shared.addCommand(.now, cmd, resp) await TerminalItems.shared.addCommand(.now, .string(composeState.message), resp)
} }
} else { } else {
let cmd = composeState.message
DispatchQueue.global().async { DispatchQueue.global().async {
Task { Task {
composeState.inProgress = true await MainActor.run { composeState.inProgress = true }
_ = await chatSendCmd(cmd) await sendTerminalCmd(cmd)
composeState.inProgress = false 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 { struct TerminalView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let chatModel = ChatModel() let chatModel = ChatModel()
chatModel.terminalItems = [ chatModel.terminalItems = [
.resp(.now, ChatResponse.response(type: "contactSubscribed", json: "{}")), .err(.now, APIResult<ChatResponse2>.invalid(type: "contactSubscribed", json: "{}".data(using: .utf8)!).unexpected),
.resp(.now, ChatResponse.response(type: "newChatItems", json: "{}")) .err(.now, APIResult<ChatResponse2>.invalid(type: "newChatItems", json: "{}".data(using: .utf8)!).unexpected)
] ]
return NavigationView { return NavigationView {
TerminalView() TerminalView()

View file

@ -47,8 +47,7 @@ enum NSEChatCommand: ChatCmdProtocol {
} }
} }
enum NSEChatResponse: Decodable, Error, ChatRespProtocol { enum NSEChatResponse: Decodable, ChatAPIResult {
case response(type: String, json: String)
case activeUser(user: User) case activeUser(user: User)
case chatStarted case chatStarted
case chatRunning case chatRunning
@ -57,11 +56,9 @@ enum NSEChatResponse: Decodable, Error, ChatRespProtocol {
case connNtfMessages(receivedMsgs: [NtfMsgInfo?]) case connNtfMessages(receivedMsgs: [NtfMsgInfo?])
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
case cmdOk(user_: UserRef?) case cmdOk(user_: UserRef?)
case chatCmdError(user_: UserRef?, chatError: ChatError)
var responseType: String { var responseType: String {
switch self { switch self {
case let .response(type, _): "* \(type)"
case .activeUser: "activeUser" case .activeUser: "activeUser"
case .chatStarted: "chatStarted" case .chatStarted: "chatStarted"
case .chatRunning: "chatRunning" case .chatRunning: "chatRunning"
@ -70,13 +67,11 @@ enum NSEChatResponse: Decodable, Error, ChatRespProtocol {
case .connNtfMessages: "connNtfMessages" case .connNtfMessages: "connNtfMessages"
case .ntfMessage: "ntfMessage" case .ntfMessage: "ntfMessage"
case .cmdOk: "cmdOk" case .cmdOk: "cmdOk"
case .chatCmdError: "chatCmdError"
} }
} }
var details: String { var details: String {
switch self { switch self {
case let .response(_, json): return json
case let .activeUser(user): return String(describing: user) case let .activeUser(user): return String(describing: user)
case .chatStarted: return noDetails case .chatStarted: return noDetails
case .chatRunning: 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 .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 let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))")
case .cmdOk: return noDetails 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 { enum NSEChatEvent: Decodable, ChatAPIResult {
case event(type: String, json: String)
case chatSuspended case chatSuspended
case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?)
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest)
@ -148,11 +94,9 @@ enum NSEChatEvent: Decodable, Error, ChatEventProtocol {
case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer) case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer)
case callInvitation(callInvitation: RcvCallInvitation) case callInvitation(callInvitation: RcvCallInvitation)
case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgAckInfo)
case chatError(user_: UserRef?, chatError: ChatError)
var eventType: String { var responseType: String {
switch self { switch self {
case let .event(type, _): "* \(type)"
case .chatSuspended: "chatSuspended" case .chatSuspended: "chatSuspended"
case .contactConnected: "contactConnected" case .contactConnected: "contactConnected"
case .receivedContactRequest: "receivedContactRequest" case .receivedContactRequest: "receivedContactRequest"
@ -162,13 +106,11 @@ enum NSEChatEvent: Decodable, Error, ChatEventProtocol {
case .sndFileRcvCancelled: "sndFileRcvCancelled" case .sndFileRcvCancelled: "sndFileRcvCancelled"
case .callInvitation: "callInvitation" case .callInvitation: "callInvitation"
case .ntfMessage: "ntfMessage" case .ntfMessage: "ntfMessage"
case .chatError: "chatError"
} }
} }
var details: String { var details: String {
switch self { switch self {
case let .event(_, json): return json
case .chatSuspended: return noDetails case .chatSuspended: return noDetails
case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact))
case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest)) 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 .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .callInvitation(inv): return String(describing: inv) 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 .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 { func receiveMsg() async {
if let msg = await chatRecvMsg() { switch await chatRecvMsg() {
case let .result(msg):
logger.debug("NotificationService receiveMsg: message") logger.debug("NotificationService receiveMsg: message")
if let (id, ntf) = await receivedMsgNtf(msg) { if let (id, ntf) = await receivedMsgNtf(msg) {
logger.debug("NotificationService receiveMsg: notification") logger.debug("NotificationService receiveMsg: notification")
await NSEThreads.shared.processNotification(id, ntf) 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 await withCheckedContinuation { cont in
let resp: NSEChatEvent? = recvSimpleXMsg() let resp: APIResult<NSEChatEvent>? = recvSimpleXMsg()
cont.resume(returning: resp) cont.resume(returning: resp)
} }
} }
@ -800,7 +806,7 @@ private let isInChina = SKStorefront().countryCode == "CHN"
private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() } private func useCallKit() -> Bool { !isInChina && callKitEnabledGroupDefault.get() }
func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? { func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? {
logger.debug("NotificationService receivedMsgNtf: \(res.eventType)") logger.debug("NotificationService receivedMsgNtf: \(res.responseType)")
switch res { switch res {
case let .contactConnected(user, contact, _): case let .contactConnected(user, contact, _):
return (contact.id, .contactConnected(user, contact)) return (contact.id, .contactConnected(user, contact))
@ -845,11 +851,8 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)?
case .chatSuspended: case .chatSuspended:
chatSuspended() chatSuspended()
return nil return nil
case let .chatError(_, err):
logger.error("NotificationService receivedMsgNtf error: \(String(describing: err))")
return nil
default: default:
logger.debug("NotificationService receivedMsgNtf ignored event: \(res.eventType)") logger.debug("NotificationService receivedMsgNtf ignored event: \(res.responseType)")
return nil return nil
} }
} }
@ -868,14 +871,14 @@ func updateNetCfg() {
} }
func apiGetActiveUser() -> User? { func apiGetActiveUser() -> User? {
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.showActiveUser) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.showActiveUser)
logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)") logger.debug("apiGetActiveUser sendSimpleXCmd response: \(r.responseType)")
switch r { switch r {
case let .activeUser(user): return user case let .result(.activeUser(user)): return user
case .chatCmdError(_, .error(.noActiveUser)): case .error(.error(.noActiveUser)):
logger.debug("apiGetActiveUser sendSimpleXCmd no active user") logger.debug("apiGetActiveUser sendSimpleXCmd no active user")
return nil return nil
case let .chatCmdError(_, err): case let .error(err):
logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))") logger.debug("apiGetActiveUser sendSimpleXCmd error: \(String(describing: err))")
return nil return nil
default: default:
@ -885,39 +888,39 @@ func apiGetActiveUser() -> User? {
} }
func apiStartChat() throws -> Bool { 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 { switch r {
case .chatStarted: return true case .result(.chatStarted): return true
case .chatRunning: return false case .result(.chatRunning): return false
default: throw r default: throw r.unexpected
} }
} }
func apiActivateChat() -> Bool { func apiActivateChat() -> Bool {
chatReopenStore() chatReopenStore()
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiActivateChat(restoreChat: false)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiActivateChat(restoreChat: false))
if case .cmdOk = r { return true } if case .result(.cmdOk) = r { return true }
logger.error("NotificationService apiActivateChat error: \(String(describing: r))") logger.error("NotificationService apiActivateChat error: \(String(describing: r))")
return false return false
} }
func apiSuspendChat(timeoutMicroseconds: Int) -> Bool { func apiSuspendChat(timeoutMicroseconds: Int) -> Bool {
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSuspendChat(timeoutMicroseconds: timeoutMicroseconds))
if case .cmdOk = r { return true } if case .result(.cmdOk) = r { return true }
logger.error("NotificationService apiSuspendChat error: \(String(describing: r))") logger.error("NotificationService apiSuspendChat error: \(String(describing: r))")
return false return false
} }
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws {
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
throw r throw r.unexpected
} }
func apiSetEncryptLocalFiles(_ enable: Bool) throws { func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSetEncryptLocalFiles(enable: enable)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
throw r throw r.unexpected
} }
func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [NtfConn]? { func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [NtfConn]? {
@ -925,11 +928,11 @@ func apiGetNtfConns(nonce: String, encNtfInfo: String) -> [NtfConn]? {
logger.debug("no active user") logger.debug("no active user")
return nil return nil
} }
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiGetNtfConns(nonce: nonce, encNtfInfo: encNtfInfo))
if case let .ntfConns(ntfConns) = r { if case let .result(.ntfConns(ntfConns)) = r {
logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)") logger.debug("apiGetNtfConns response ntfConns: \(ntfConns.count)")
return ntfConns 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))") logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))")
} else { } else {
logger.debug("apiGetNtfMessage ignored response: \(r.responseType) \(String.init(describing: r))") logger.debug("apiGetNtfMessage ignored response: \(r.responseType) \(String.init(describing: r))")
@ -943,12 +946,12 @@ func apiGetConnNtfMessages(connMsgReqs: [ConnMsgReq]) -> [NtfMsgInfo?]? {
return nil return nil
} }
logger.debug("apiGetConnNtfMessages command: \(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs).cmdString)") logger.debug("apiGetConnNtfMessages command: \(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs).cmdString)")
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiGetConnNtfMessages(connMsgReqs: connMsgReqs))
if case let .connNtfMessages(receivedMsgs) = r { if case let .result(.connNtfMessages(receivedMsgs)) = r {
logger.debug("apiGetConnNtfMessages response receivedMsgs: total \(receivedMsgs.count), expecting messages \(receivedMsgs.count { $0 != nil })") logger.debug("apiGetConnNtfMessages response receivedMsgs: total \(receivedMsgs.count), expecting messages \(receivedMsgs.count { $0 != nil })")
return receivedMsgs return receivedMsgs
} }
logger.debug("apiGetConnNtfMessages error: \(responseError(r))") logger.debug("apiGetConnNtfMessages error: \(responseError(r.unexpected))")
return nil return nil
} }
@ -962,17 +965,17 @@ func getConnNtfMessage(connMsgReq: ConnMsgReq) -> NtfMsgInfo? {
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? { func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case let .result(.rcvFileAccepted(_, chatItem)) = r { return chatItem }
logger.error("receiveFile error: \(responseError(r))") logger.error("receiveFile error: \(responseError(r.unexpected))")
return nil return nil
} }
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) { func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
logger.error("setFileToReceive error: \(responseError(r))") logger.error("setFileToReceive error: \(responseError(r.unexpected))")
} }
func autoReceiveFile(_ file: CIFile) -> ChatItem? { func autoReceiveFile(_ file: CIFile) -> ChatItem? {
@ -989,9 +992,9 @@ func autoReceiveFile(_ file: CIFile) -> ChatItem? {
} }
func setNetworkConfig(_ cfg: NetCfg) throws { func setNetworkConfig(_ cfg: NetCfg) throws {
let r: NSEChatResponse = sendSimpleXCmd(NSEChatCommand.apiSetNetworkConfig(networkConfig: cfg)) let r: APIResult<NSEChatResponse> = sendSimpleXCmd(NSEChatCommand.apiSetNetworkConfig(networkConfig: cfg))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
throw r throw r.unexpected
} }
func defaultBestAttemptNtf(_ ntfConn: NtfConn) -> NSENotificationData { func defaultBestAttemptNtf(_ ntfConn: NtfConn) -> NSENotificationData {

View file

@ -13,52 +13,52 @@ import SimpleXChat
let logger = Logger() let logger = Logger()
func apiGetActiveUser() throws -> User? { func apiGetActiveUser() throws -> User? {
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.showActiveUser) let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.showActiveUser)
switch r { switch r {
case let .activeUser(user): return user case let .result(.activeUser(user)): return user
case .chatCmdError(_, .error(.noActiveUser)): return nil case .error(.error(.noActiveUser)): return nil
default: throw r default: throw r.unexpected
} }
} }
func apiStartChat() throws -> Bool { 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 { switch r {
case .chatStarted: return true case .result(.chatStarted): return true
case .chatRunning: return false case .result(.chatRunning): return false
default: throw r default: throw r.unexpected
} }
} }
func apiSetNetworkConfig(_ cfg: NetCfg) throws { func apiSetNetworkConfig(_ cfg: NetCfg) throws {
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetNetworkConfig(networkConfig: cfg)) let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetNetworkConfig(networkConfig: cfg))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
throw r throw r.unexpected
} }
func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws {
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
throw r throw r.unexpected
} }
func apiSetEncryptLocalFiles(_ enable: Bool) throws { func apiSetEncryptLocalFiles(_ enable: Bool) throws {
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSetEncryptLocalFiles(enable: enable)) let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSetEncryptLocalFiles(enable: enable))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
throw r throw r.unexpected
} }
func apiGetChats(userId: User.ID) throws -> Array<ChatData> { func apiGetChats(userId: User.ID) throws -> Array<ChatData> {
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiGetChats(userId: userId)) let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiGetChats(userId: userId))
if case let .apiChats(user: _, chats: chats) = r { return chats } if case let .result(.apiChats(user: _, chats: chats)) = r { return chats }
throw r throw r.unexpected
} }
func apiSendMessages( func apiSendMessages(
chatInfo: ChatInfo, chatInfo: ChatInfo,
composedMessages: [ComposedMessage] composedMessages: [ComposedMessage]
) throws -> [AChatItem] { ) throws -> [AChatItem] {
let r: SEChatResponse = sendSimpleXCmd( let r: APIResult<SEChatResponse> = sendSimpleXCmd(
chatInfo.chatType == .local chatInfo.chatType == .local
? SEChatCommand.apiCreateChatItems( ? SEChatCommand.apiCreateChatItems(
noteFolderId: chatInfo.apiId, noteFolderId: chatInfo.apiId,
@ -72,33 +72,33 @@ func apiSendMessages(
composedMessages: composedMessages composedMessages: composedMessages
) )
) )
if case let .newChatItems(_, chatItems) = r { if case let .result(.newChatItems(_, chatItems)) = r {
return chatItems return chatItems
} else { } else {
for composedMessage in composedMessages { for composedMessage in composedMessages {
if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) } if let filePath = composedMessage.fileSource?.filePath { removeFile(filePath) }
} }
throw r throw r.unexpected
} }
} }
func apiActivateChat() throws { func apiActivateChat() throws {
chatReopenStore() chatReopenStore()
let r: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false)) let r: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiActivateChat(restoreChat: false))
if case .cmdOk = r { return } if case .result(.cmdOk) = r { return }
throw r throw r.unexpected
} }
func apiSuspendChat(expired: Bool) { 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 // Block until `chatSuspended` received or 3 seconds has passed
var suspended = false var suspended = false
if case .cmdOk = r, !expired { if case .result(.cmdOk) = r, !expired {
let startTime = CFAbsoluteTimeGetCurrent() let startTime = CFAbsoluteTimeGetCurrent()
while CFAbsoluteTimeGetCurrent() - startTime < 3 { while CFAbsoluteTimeGetCurrent() - startTime < 3 {
let msg: SEChatEvent? = recvSimpleXMsg(messageTimeout: 3_500000) let msg: APIResult<SEChatEvent>? = recvSimpleXMsg(messageTimeout: 3_500000)
switch msg { switch msg {
case .chatSuspended: case .result(.chatSuspended):
suspended = false suspended = false
break break
default: continue default: continue
@ -106,7 +106,7 @@ func apiSuspendChat(expired: Bool) {
} }
} }
if !suspended { if !suspended {
let _r1: SEChatResponse = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: 0)) let _r1: APIResult<SEChatResponse> = sendSimpleXCmd(SEChatCommand.apiSuspendChat(timeoutMicroseconds: 0))
} }
logger.debug("close store") logger.debug("close store")
chatCloseStore() chatCloseStore()
@ -151,32 +151,27 @@ enum SEChatCommand: ChatCmdProtocol {
} }
} }
enum SEChatResponse: Decodable, Error, ChatRespProtocol { enum SEChatResponse: Decodable, ChatAPIResult {
case response(type: String, json: String)
case activeUser(user: User) case activeUser(user: User)
case chatStarted case chatStarted
case chatRunning case chatRunning
case apiChats(user: UserRef, chats: [ChatData]) case apiChats(user: UserRef, chats: [ChatData])
case newChatItems(user: UserRef, chatItems: [AChatItem]) case newChatItems(user: UserRef, chatItems: [AChatItem])
case cmdOk(user_: UserRef?) case cmdOk(user_: UserRef?)
case chatCmdError(user_: UserRef?, chatError: ChatError)
var responseType: String { var responseType: String {
switch self { switch self {
case let .response(type, _): "* \(type)"
case .activeUser: "activeUser" case .activeUser: "activeUser"
case .chatStarted: "chatStarted" case .chatStarted: "chatStarted"
case .chatRunning: "chatRunning" case .chatRunning: "chatRunning"
case .apiChats: "apiChats" case .apiChats: "apiChats"
case .newChatItems: "newChatItems" case .newChatItems: "newChatItems"
case .cmdOk: "cmdOk" case .cmdOk: "cmdOk"
case .chatCmdError: "chatCmdError"
} }
} }
var details: String { var details: String {
switch self { switch self {
case let .response(_, json): return json
case let .activeUser(user): return String(describing: user) case let .activeUser(user): return String(describing: user)
case .chatStarted: return noDetails case .chatStarted: return noDetails
case .chatRunning: 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") let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
return withUser(u, itemsString) return withUser(u, itemsString)
case .cmdOk: return noDetails case .cmdOk: return noDetails
case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError))
} }
} }
var noDetails: String { "\(responseType): no details" } static func fallbackResult(_ type: String, _ json: NSDictionary) -> SEChatResponse? {
if type == "apiChats", let r = parseApiChats(json) {
static func chatResponse(_ s: String) -> SEChatResponse { .apiChats(user: r.user, chats: r.chats)
let d = s.data(using: .utf8)! } else {
// TODO is there a way to do it without copying the data? e.g: nil
// 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
} }
} }
} }
enum SEChatEvent: Decodable, Error, ChatEventProtocol { enum SEChatEvent: Decodable, ChatAPIResult {
case event(type: String, json: String)
case chatSuspended case chatSuspended
case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem])
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String)
case sndFileWarning(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 { switch self {
case let .event(type, _): "* \(type)"
case .chatSuspended: "chatSuspended" case .chatSuspended: "chatSuspended"
case .sndFileProgressXFTP: "sndFileProgressXFTP" case .sndFileProgressXFTP: "sndFileProgressXFTP"
case .sndFileCompleteXFTP: "sndFileCompleteXFTP" case .sndFileCompleteXFTP: "sndFileCompleteXFTP"
case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated"
case .sndFileError: "sndFileError" case .sndFileError: "sndFileError"
case .sndFileWarning: "sndFileWarning" case .sndFileWarning: "sndFileWarning"
case .chatError: "chatError"
} }
} }
var details: String { var details: String {
switch self { switch self {
case let .event(_, json): return json
case .chatSuspended: return noDetails case .chatSuspended: return noDetails
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)") 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)) case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
@ -275,53 +221,6 @@ enum SEChatEvent: Decodable, Error, ChatEventProtocol {
return withUser(u, itemsString) return withUser(u, itemsString)
case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") 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 .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 { switch r {
case let .sndFileProgressXFTP(_, ci, _, sentSize, totalSize): case let .result(.sndFileProgressXFTP(_, ci, _, sentSize, totalSize)):
guard isMessage(for: ci) else { continue } guard isMessage(for: ci) else { continue }
networkTimeout = CFAbsoluteTimeGetCurrent() networkTimeout = CFAbsoluteTimeGetCurrent()
await MainActor.run { await MainActor.run {
@ -314,14 +314,14 @@ class ShareModel: ObservableObject {
bottomBar = .loadingBar(progress: progress) bottomBar = .loadingBar(progress: progress)
} }
} }
case let .sndFileCompleteXFTP(_, ci, _): case let .result(.sndFileCompleteXFTP(_, ci, _)):
guard isMessage(for: ci) else { continue } guard isMessage(for: ci) else { continue }
if isGroupChat { if isGroupChat {
await MainActor.run { bottomBar = .loadingSpinner } await MainActor.run { bottomBar = .loadingSpinner }
} }
await ch.completeFile() await ch.completeFile()
if await !ch.isRunning { break } if await !ch.isRunning { break }
case let .chatItemsStatusesUpdated(_, chatItems): case let .result(.chatItemsStatusesUpdated(_, chatItems)):
guard let ci = chatItems.last else { continue } guard let ci = chatItems.last else { continue }
guard isMessage(for: ci) else { continue } guard isMessage(for: ci) else { continue }
if let (title, message) = ci.chatItem.meta.itemStatus.statusInfo { 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 } guard isMessage(for: ci) else { continue }
if let ci { cleanupFile(ci) } if let ci { cleanupFile(ci) }
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") 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 } guard isMessage(for: ci) else { continue }
if let ci { cleanupFile(ci) } if let ci { cleanupFile(ci) }
return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)")
case let .chatError(_, chatError): case let .error(chatError):
return ErrorAlert(chatError) return ErrorAlert(chatError)
default: continue default: continue
} }

View file

@ -46,7 +46,7 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
var cConfirm = confirm.rawValue.cString(using: .utf8)! var cConfirm = confirm.rawValue.cString(using: .utf8)!
// the last parameter of chat_migrate_init is used to return the pointer to chat controller // 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 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 encrypted = dbKey != ""
let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey) let keychainErr = dbRes == .ok && useKeychain && encrypted && !kcDatabasePassword.set(dbKey)
let result = (encrypted, keychainErr ? .errorKeychain : dbRes) 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 cKey = dbKey.cString(using: .utf8)!
var cConfirm = confirmation.rawValue.cString(using: .utf8)! var cConfirm = confirmation.rawValue.cString(using: .utf8)!
let cjson = chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &temporaryController)! 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() { public func chatInitControllerRemovingDatabases() {
@ -110,27 +110,42 @@ public func resetChatCtrl() {
migrationResult = nil migrationResult = nil
} }
public func sendSimpleXCmd<CR: ChatRespProtocol>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> CR { @inline(__always)
var c = cmd.cmdString.cString(using: .utf8)! public func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult<R> {
let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)! if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl) {
return CR.chatResponse(fromCString(cjson)) 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 // in microseconds
public let MESSAGE_TIMEOUT: Int32 = 15_000_000 public let MESSAGE_TIMEOUT: Int32 = 15_000_000
public func recvSimpleXMsg<CEvt: ChatEventProtocol>(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> CEvt? { @inline(__always)
if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) { public func recvSimpleXMsg<R: ChatAPIResult>(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> APIResult<R>? {
let s = fromCString(cjson) if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout),
return s == "" ? nil : CEvt.chatEvent(s) let d = dataFromCString(cjson) {
decodeAPIResult(d)
} else {
nil
} }
return nil
} }
public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? { public func parseSimpleXMarkdown(_ s: String) -> [FormattedText]? {
var c = s.cString(using: .utf8)! var c = s.cString(using: .utf8)!
if let cjson = chat_parse_markdown(&c) { if let cjson = chat_parse_markdown(&c) {
if let d = fromCString(cjson).data(using: .utf8) { if let d = dataFromCString(cjson) {
do { do {
let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d) let r = try jsonDecoder.decode(ParsedMarkdown.self, from: d)
return r.formattedText return r.formattedText
@ -154,7 +169,7 @@ struct ParsedMarkdown: Decodable {
public func parseServerAddress(_ s: String) -> ServerAddress? { public func parseServerAddress(_ s: String) -> ServerAddress? {
var c = s.cString(using: .utf8)! var c = s.cString(using: .utf8)!
if let cjson = chat_parse_server(&c) { if let cjson = chat_parse_server(&c) {
if let d = fromCString(cjson).data(using: .utf8) { if let d = dataFromCString(cjson) {
do { do {
let r = try jsonDecoder.decode(ParsedServerAddress.self, from: d) let r = try jsonDecoder.decode(ParsedServerAddress.self, from: d)
return r.serverAddress return r.serverAddress
@ -171,12 +186,33 @@ struct ParsedServerAddress: Decodable {
var parseError: String var parseError: String
} }
@inline(__always)
public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String { public func fromCString(_ c: UnsafeMutablePointer<CChar>) -> String {
let s = String.init(cString: c) let s = String.init(cString: c)
free(c) free(c)
return s 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? { public func decodeUser_(_ jDict: NSDictionary) -> UserRef? {
if let user_ = jDict["user_"] { if let user_ = jDict["user_"] {
try? decodeObject(user_ as Any) 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"] { if let chatError = jDict["chatError"] {
serializeJSON(chatError) serializeJSON(chatError)
} else { } else {
@ -197,7 +233,11 @@ public func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatD
let jChatDict = jChat as! NSDictionary let jChatDict = jChat as! NSDictionary
let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!) let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!)
let chatStats: ChatStats = try decodeObject(jChatDict["chatStats"]!) 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 jChatItems = jChatDict["chatItems"] as! NSArray
let chatItems = jChatItems.map { jCI in let chatItems = jChatItems.map { jCI in
if let ci: ChatItem = try? decodeObject(jCI) { if let ci: ChatItem = try? decodeObject(jCI) {
@ -206,16 +246,18 @@ public func parseChatData(_ jChat: Any, _ jNavInfo: Any? = nil) throws -> (ChatD
return ChatItem.invalidJSON( return ChatItem.invalidJSON(
chatDir: decodeProperty(jCI, "chatDir"), chatDir: decodeProperty(jCI, "chatDir"),
meta: decodeProperty(jCI, "meta"), meta: decodeProperty(jCI, "meta"),
json: serializeJSON(jCI, options: .prettyPrinted) ?? "" json: serializeJSON(jCI, options: .prettyPrinted)
) )
} }
return (ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats), navInfo) return (ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats), navInfo)
} }
@inline(__always)
public func decodeObject<T: Decodable>(_ obj: Any) throws -> T { public func decodeObject<T: Decodable>(_ obj: Any) throws -> T {
try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj)) try jsonDecoder.decode(T.self, from: JSONSerialization.data(withJSONObject: obj))
} }
@inline(__always)
func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? { func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? {
if let jProp = (obj as? NSDictionary)?[prop] { if let jProp = (obj as? NSDictionary)?[prop] {
return try? decodeObject(jProp) return try? decodeObject(jProp)
@ -223,28 +265,52 @@ func decodeProperty<T: Decodable>(_ obj: Any, _ prop: NSString) -> T? {
return nil return nil
} }
public func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> String? { @inline(__always)
if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) { func getOWSF(_ obj: NSDictionary, _ prop: NSString) -> (type: String, object: NSDictionary)? {
return String(decoding: d, as: UTF8.self) 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 return nil
} }
public func responseError(_ err: Error) -> String { @inline(__always)
if let r = err as? ChatRespProtocol { public func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> Data? {
if let e = r.chatError { if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) {
chatErrorString(e) dataPrefix(d)
} else { } else {
"\(String(describing: r.responseType)), details: \(String(describing: r.details))" 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 e = err as? ChatError {
chatErrorString(e)
} else { } else {
String(describing: err) String(describing: err)
} }
} }
public func chatErrorString(_ err: ChatError) -> String { public func chatErrorString(_ err: ChatError) -> String {
if case let .invalidJSON(json) = err { return json } switch err {
return String(describing: err) case let .invalidJSON(json): dataToString(json)
case let .unexpectedResult(type): "unexpected result: \(type)"
default: String(describing: err)
}
} }
public enum DBMigrationResult: Decodable, Equatable { public enum DBMigrationResult: Decodable, Equatable {
@ -283,15 +349,15 @@ public enum MTRError: Decodable, Equatable {
case different(appMigration: String, dbMigration: String) case different(appMigration: String, dbMigration: String)
} }
func dbMigrationResult(_ s: String) -> DBMigrationResult { func dbMigrationResult(_ d: Data?) -> DBMigrationResult {
let d = s.data(using: .utf8)! if let d {
// 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 { do {
return try jsonDecoder.decode(DBMigrationResult.self, from: d) return try jsonDecoder.decode(DBMigrationResult.self, from: d)
} catch let error { } catch let error {
logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)") logger.error("chatResponse jsonDecoder.decode error: \(error.localizedDescription)")
return .unknown(json: s) return .unknown(json: dataToString(d))
}
} else {
return .unknown(json: "no data")
} }
} }

View file

@ -17,30 +17,117 @@ public protocol ChatCmdProtocol {
var cmdString: String { get } var cmdString: String { get }
} }
@inline(__always)
public func onOff(_ b: Bool) -> String { public func onOff(_ b: Bool) -> String {
b ? "on" : "off" b ? "on" : "off"
} }
public struct APIResponse<ChatRespProtocol: Decodable>: Decodable { public enum APIResult<R>: Decodable where R: Decodable, R: ChatAPIResult {
public var resp: ChatRespProtocol 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 protocol ChatRespProtocol: Decodable, Error { 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 ChatAPIResult: Decodable {
var responseType: String { get } var responseType: String { get }
var details: String { get } var details: String { get }
static func chatResponse(_ s: String) -> Self static func fallbackResult(_ type: String, _ json: NSDictionary) -> Self?
var chatError: ChatError? { get }
var chatErrorType: ChatErrorType? { get }
} }
public protocol ChatEventProtocol: Decodable, Error { extension ChatAPIResult {
var eventType: String { get } public var noDetails: String { "\(self.responseType): no details" }
var details: String { get }
static func chatEvent(_ s: String) -> Self @inline(__always)
var chatError: ChatError? { get } public static func fallbackResult(_ type: String, _ json: NSDictionary) -> Self? {
var chatErrorType: ChatErrorType? { get } 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])? { public func parseApiChats(_ jResp: NSDictionary) -> (user: UserRef, chats: [ChatData])? {
if let jApiChats = jResp["apiChats"] as? NSDictionary, if let jApiChats = jResp["apiChats"] as? NSDictionary,
let user: UserRef = try? decodeObject(jApiChats["user"] as Any), 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) { if let chatData = try? parseChatData(jChat) {
return chatData.0 return chatData.0
} }
return ChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted) ?? "") return ChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted))
} }
return (user, chats) return (user, chats)
} else { } else {
@ -553,13 +640,26 @@ private func encodeCJSON<T: Encodable>(_ value: T) -> [CChar] {
encodeJSON(value).cString(using: .utf8)! encodeJSON(value).cString(using: .utf8)!
} }
public enum ChatError: Decodable, Hashable { public enum ChatError: Decodable, Hashable, Error {
case error(errorType: ChatErrorType) case error(errorType: ChatErrorType)
case errorAgent(agentError: AgentErrorType) case errorAgent(agentError: AgentErrorType)
case errorStore(storeError: StoreError) case errorStore(storeError: StoreError)
case errorDatabase(databaseError: DatabaseError) case errorDatabase(databaseError: DatabaseError)
case errorRemoteCtrl(remoteCtrlError: RemoteCtrlError) 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 { public enum ChatErrorType: Decodable, Hashable {

View file

@ -1201,7 +1201,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable {
case local(noteFolder: NoteFolder) case local(noteFolder: NoteFolder)
case contactRequest(contactRequest: UserContactRequest) case contactRequest(contactRequest: UserContactRequest)
case contactConnection(contactConnection: PendingContactConnection) case contactConnection(contactConnection: PendingContactConnection)
case invalidJSON(json: String) case invalidJSON(json: Data?)
private static let invalidChatName = NSLocalizedString("invalid chat", comment: "invalid chat 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 self.chatStats = chatStats
} }
public static func invalidJSON(_ json: String) -> ChatData { public static func invalidJSON(_ json: Data?) -> ChatData {
ChatData( ChatData(
chatInfo: .invalidJSON(json: json), chatInfo: .invalidJSON(json: json),
chatItems: [], chatItems: [],
@ -2905,7 +2905,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable {
return item 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( ChatItem(
chatDir: chatDir ?? .directSnd, chatDir: chatDir ?? .directSnd,
meta: meta ?? .invalidJSON, meta: meta ?? .invalidJSON,
@ -3352,7 +3352,7 @@ public enum CIContent: Decodable, ItemContent, Hashable {
case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo) case rcvDirectE2EEInfo(e2eeInfo: E2EEInfo)
case sndGroupE2EEInfo(e2eeInfo: E2EEInfo) case sndGroupE2EEInfo(e2eeInfo: E2EEInfo)
case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo) case rcvGroupE2EEInfo(e2eeInfo: E2EEInfo)
case invalidJSON(json: String) case invalidJSON(json: Data?)
public var text: String { public var text: String {
get { get {

View file

@ -18,10 +18,10 @@ public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs {
memcpy(ptr, (data as NSData).bytes, data.count) memcpy(ptr, (data as NSData).bytes, data.count)
var cPath = path.cString(using: .utf8)! var cPath = path.cString(using: .utf8)!
let cjson = chat_write_file(getChatCtrl(), &cPath, ptr, Int32(data.count))! 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) { switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
case let .result(cfArgs): return cfArgs 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 cFromPath = fromPath.cString(using: .utf8)!
var cToPath = toPath.cString(using: .utf8)! var cToPath = toPath.cString(using: .utf8)!
let cjson = chat_encrypt_file(getChatCtrl(), &cFromPath, &cToPath)! 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) { switch try jsonDecoder.decode(WriteFileResult.self, from: d) {
case let .result(cfArgs): return cfArgs 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) { public init(_ error: any Error) {
self = if let chatResponse = error as? ChatRespProtocol { self = if let e = error as? ChatError {
ErrorAlert(chatResponse) ErrorAlert(e)
} else { } else {
ErrorAlert("\(error.localizedDescription)") ErrorAlert("\(error.localizedDescription)")
} }
} }
public init(_ chatError: ChatError) { public init(_ chatError: ChatError) {
self = ErrorAlert("\(chatErrorString(chatError))") self = if let networkErrorAlert = getNetworkErrorAlert(chatError) {
}
public init(_ chatResponse: ChatRespProtocol) {
self = if let networkErrorAlert = getNetworkErrorAlert(chatResponse) {
networkErrorAlert networkErrorAlert
} else { } else {
ErrorAlert("\(responseError(chatResponse))") ErrorAlert("\(chatErrorString(chatError))")
} }
} }
} }
@ -94,8 +90,8 @@ extension View {
} }
} }
public func getNetworkErrorAlert(_ r: ChatRespProtocol) -> ErrorAlert? { public func getNetworkErrorAlert(_ e: ChatError) -> ErrorAlert? {
switch r.chatError { switch e {
case let .errorAgent(.BROKER(addr, .TIMEOUT)): case let .errorAgent(.BROKER(addr, .TIMEOUT)):
ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.")
case let .errorAgent(.BROKER(addr, .NETWORK)): case let .errorAgent(.BROKER(addr, .NETWORK)):

View file

@ -229,5 +229,5 @@ fun isMediaIntent(intent: Intent): Boolean =
// val str: String = """ // val str: String = """
// """.trimIndent() // """.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.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.* import java.util.*
import java.util.concurrent.atomic.AtomicLong
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.random.Random import kotlin.random.Random
import kotlin.time.* import kotlin.time.*
@ -1396,19 +1397,21 @@ sealed class ChatInfo: SomeChat, NamedChat {
} }
@Serializable @SerialName("invalidJSON") @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 chatType get() = ChatType.Direct
override val localDisplayName get() = invalidChatName override val localDisplayName get() = invalidChatName
override val id get() = "" override val id get() = "?$apiId"
override val apiId get() = 0L
override val ready get() = false override val ready get() = false
override val chatDeleted get() = false override val chatDeleted get() = false
override val sendMsgEnabled get() = false override val sendMsgEnabled get() = false
override val incognito get() = false override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = false override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null 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 displayName get() = invalidChatName
override val fullName get() = invalidChatName override val fullName get() = invalidChatName
override val image get() = null override val image get() = null
@ -1416,6 +1419,7 @@ sealed class ChatInfo: SomeChat, NamedChat {
companion object { companion object {
private val invalidChatName = generalGetString(MR.strings.invalid_chat) 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 prefPerformLA = chatModel.controller.appPrefs.performLA.get()
val s = composeState.value.message val s = composeState.value.message
if (s.text.startsWith("/sql") && (!prefPerformLA || !developerTools)) { 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.cmd(null, CC.Console(s.text)))
chatModel.addTerminalItem(TerminalItem.resp(null, resp)) chatModel.addTerminalItem(TerminalItem.resp(null, resp))
composeState.value = ComposeState(useLinkPreviews = false) composeState.value = ComposeState(useLinkPreviews = false)

View file

@ -10,13 +10,13 @@ import kotlinx.coroutines.*
expect fun ActiveCallView() expect fun ActiveCallView()
fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatchers.Default) { fun activeCallWaitDeliveryReceipt(scope: CoroutineScope) = scope.launch(Dispatchers.Default) {
for (apiResp in controller.messagesChannel) { for (msg in controller.messagesChannel) {
val call = chatModel.activeCall.value val call = chatModel.activeCall.value
if (call == null || call.callState > CallState.InvitationSent) break if (call == null || call.callState > CallState.InvitationSent) break
val msg = apiResp.resp if (msg.rhId == call.remoteHostId &&
if (apiResp.remoteHostId == call.remoteHostId && msg is API.Result &&
msg is CR.ChatItemsStatusesUpdated && msg.res is CR.ChatItemsStatusesUpdated &&
msg.chatItems.any { msg.res.chatItems.any {
it.chatInfo.id == call.contact.id && it.chatItem.content is CIContent.SndCall && it.chatItem.meta.itemStatus is CIStatus.SndRcvd 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) ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get(), chatInfo)
LaunchedEffect(cItem.id) { LaunchedEffect(cItem.id) {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
for (apiResp in controller.messagesChannel) { for (msg in controller.messagesChannel) {
val msg = apiResp.resp if (msg.rhId == chatRh &&
if (apiResp.remoteHostId == chatRh && msg is API.Result &&
msg is CR.ChatItemsStatusesUpdated && msg.res is CR.ChatItemsStatusesUpdated &&
msg.chatItems.any { it.chatItem.id == cItem.id } msg.res.chatItems.any { it.chatItem.id == cItem.id }
) { ) {
ciInfo = loadChatItemInfo() ?: return@withContext ciInfo = loadChatItemInfo() ?: return@withContext
initialCiInfo = ciInfo initialCiInfo = ciInfo

View file

@ -435,7 +435,7 @@ suspend fun encryptDatabase(
} }
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
appPrefs.encryptionStartedAt.set(null) 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 { when {
sqliteError is SQLiteError.ErrorNotADatabase -> { sqliteError is SQLiteError.ErrorNotADatabase -> {
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
@ -449,7 +449,7 @@ suspend fun encryptDatabase(
error != null -> { error != null -> {
operationEnded(m, progressIndicator) { operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), 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 false

View file

@ -468,12 +468,12 @@ private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(db
val error = controller.testStorageEncryption(dbKey) val error = controller.testStorageEncryption(dbKey)
if (error == null) { if (error == null) {
state = MigrationFromState.UploadConfirmation 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("")) showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase(""))
} else { } else {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.error), 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 { withBGApi {
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg -> chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) { val r = msg.result
when (r) {
is CR.SndFileProgressXFTP -> { is CR.SndFileProgressXFTP -> {
val s = state val s = state
if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) { 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 -> { is CR.SndFileRedirectStartXFTP -> {
@ -578,7 +579,7 @@ private fun MutableState<MigrationFromState>.startUploading(
requiredHostMode = cfg.requiredHostMode 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 -> { is CR.SndFileError -> {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
@ -692,7 +693,7 @@ private class MigrationFromChatReceiver(
val ctrl: ChatCtrl, val ctrl: ChatCtrl,
val databaseUrl: File, val databaseUrl: File,
var receiveMessages: Boolean = true, var receiveMessages: Boolean = true,
val processReceivedMsg: suspend (CR) -> Unit val processReceivedMsg: suspend (API) -> Unit
) { ) {
fun start() { fun start() {
Log.d(TAG, "MigrationChatReceiver startReceiver") Log.d(TAG, "MigrationChatReceiver startReceiver")
@ -701,19 +702,18 @@ private class MigrationFromChatReceiver(
try { try {
val msg = ChatController.recvMsg(ctrl) val msg = ChatController.recvMsg(ctrl)
if (msg != null && receiveMessages) { if (msg != null && receiveMessages) {
val r = msg.resp val rhId = msg.rhId
val rhId = msg.remoteHostId Log.d(TAG, "processReceivedMsg: ${msg.responseType}")
Log.d(TAG, "processReceivedMsg: ${r.responseType}") chatModel.addTerminalItem(TerminalItem.resp(rhId, msg))
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(r) processReceivedMsg(msg)
} }
if (finishedWithoutTimeout == null) { 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()) { if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title), 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 shareText = true
) )
} }

View file

@ -580,12 +580,13 @@ private fun MutableState<MigrationToState?>.startDownloading(
) { ) {
withBGApi { withBGApi {
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg -> chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) { val r = msg.result
is CR.RcvFileProgressXFTP -> { when {
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl) 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)) MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy))
} }
is CR.RcvStandaloneFileComplete -> { r is CR.RcvStandaloneFileComplete -> {
delay(500) delay(500)
// User closed the whole screen before new state was saved // User closed the whole screen before new state was saved
if (state == null) { if (state == null) {
@ -595,22 +596,22 @@ private fun MutableState<MigrationToState?>.startDownloading(
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy)) MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy))
} }
} }
is CR.RcvFileError -> { r is CR.RcvFileError -> {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_download_failed), generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid) generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
) )
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
} }
is CR.ChatRespError -> { msg is API.Error -> {
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) { if (msg.err is ChatError.ChatErrorChat && msg.err.errorType is ChatErrorType.NoRcvFileUser) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_download_failed), generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid) generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
) )
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy) state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
} else { } 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}") else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
@ -739,7 +740,7 @@ private class MigrationToChatReceiver(
val ctrl: ChatCtrl, val ctrl: ChatCtrl,
val databaseUrl: File, val databaseUrl: File,
var receiveMessages: Boolean = true, var receiveMessages: Boolean = true,
val processReceivedMsg: suspend (CR) -> Unit val processReceivedMsg: suspend (API) -> Unit
) { ) {
fun start() { fun start() {
Log.d(TAG, "MigrationChatReceiver startReceiver") Log.d(TAG, "MigrationChatReceiver startReceiver")
@ -748,19 +749,18 @@ private class MigrationToChatReceiver(
try { try {
val msg = ChatController.recvMsg(ctrl) val msg = ChatController.recvMsg(ctrl)
if (msg != null && receiveMessages) { if (msg != null && receiveMessages) {
val r = msg.resp val rhId = msg.rhId
val rhId = msg.remoteHostId Log.d(TAG, "processReceivedMsg: ${msg.responseType}")
Log.d(TAG, "processReceivedMsg: ${r.responseType}") chatModel.addTerminalItem(TerminalItem.resp(rhId, msg))
chatModel.addTerminalItem(TerminalItem.resp(rhId, r))
val finishedWithoutTimeout = withTimeoutOrNull(60_000L) { val finishedWithoutTimeout = withTimeoutOrNull(60_000L) {
processReceivedMsg(r) processReceivedMsg(msg)
} }
if (finishedWithoutTimeout == null) { 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()) { if (appPreferences.developerTools.get() && appPreferences.showSlowApiCalls.get()) {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.possible_slow_function_title), 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 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() val res = connect()
if (res.first != null) { if (res.first != null) {
val (rc_, ctrlAppInfo, v) = res.first!! val (rc_, ctrlAppInfo, v) = res.first!!
@ -505,13 +505,13 @@ private suspend fun connectDesktop(sessionAddress: MutableState<String>, connect
} else { } else {
val e = res.second ?: return false val e = res.second ?: return false
when { when {
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert() e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadInvitation -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorChat && e.chatError.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert() e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.CommandError -> showBadInvitationErrorAlert()
e.chatError is ChatError.ChatErrorRemoteCtrl && e.chatError.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.chatError.remoteCtrlError.appVersion) e is ChatError.ChatErrorRemoteCtrl && e.remoteCtrlError is RemoteCtrlError.BadVersion -> showBadVersionAlert(v = e.remoteCtrlError.appVersion)
e.chatError is ChatError.ChatErrorAgent && e.chatError.agentError is AgentErrorType.RCP && e.chatError.agentError.rcpErr is RCErrorType.VERSION -> showBadVersionAlert(v = null) e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.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.ChatErrorAgent && e.agentError is AgentErrorType.RCP && e.agentError.rcpErr is RCErrorType.CTRL_AUTH -> showDesktopDisconnectedErrorAlert()
else -> { else -> {
val errMsg = "${e.responseType}: ${e.details}" val errMsg = "error: ${e.string}"
Log.e(TAG, "bad response: $errMsg") Log.e(TAG, "bad response: $errMsg")
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), 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)) { SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) {
SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) { SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) {
DefaultSwitch( DefaultSwitch(
checked = currentUser.sendRcptsContacts ?: false, checked = currentUser.sendRcptsContacts,
onCheckedChange = { enable -> onCheckedChange = { enable ->
setOrAskSendReceiptsContacts(enable) setOrAskSendReceiptsContacts(enable)
} }
@ -299,7 +299,7 @@ private fun DeliveryReceiptsSection(
} }
SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) { SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) {
DefaultSwitch( DefaultSwitch(
checked = currentUser.sendRcptsSmallGroups ?: false, checked = currentUser.sendRcptsSmallGroups,
onCheckedChange = { enable -> onCheckedChange = { enable ->
setOrAskSendReceiptsGroups(enable) setOrAskSendReceiptsGroups(enable)
} }

View file

@ -45,10 +45,10 @@ mySquaringBot _user cc = do
race_ (forever $ void getLine) . forever $ do race_ (forever $ void getLine) . forever $ do
(_, evt) <- atomically . readTBQueue $ outputQ cc (_, evt) <- atomically . readTBQueue $ outputQ cc
case evt of case evt of
CEvtContactConnected _ contact _ -> do Right (CEvtContactConnected _ contact _) -> do
contactConnected contact contactConnected contact
sendMessage cc contact welcomeMessage 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 let msg = ciContentToText mc
number_ = readMaybe (T.unpack msg) :: Maybe Integer number_ = readMaybe (T.unpack msg) :: Maybe Integer
sendMessage cc contact $ case number_ of sendMessage cc contact $ case number_ of

View file

@ -39,17 +39,17 @@ broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _u
race_ (forever $ void getLine) . forever $ do race_ (forever $ void getLine) . forever $ do
(_, evt) <- atomically . readTBQueue $ outputQ cc (_, evt) <- atomically . readTBQueue $ outputQ cc
case evt of case evt of
CEvtContactConnected _ ct _ -> do Right (CEvtContactConnected _ ct _) -> do
contactConnected ct contactConnected ct
sendMessage cc ct welcomeMessage 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 | sender `notElem` publishers -> do
sendReply prohibitedMessage sendReply prohibitedMessage
deleteMessage cc ct $ chatItemId' ci deleteMessage cc ct $ chatItemId' ci
| allowContent mc -> | allowContent mc ->
void $ forkIO $ void $ forkIO $
sendChatCmd cc (SendMessageBroadcast mc) >>= \case sendChatCmd cc (SendMessageBroadcast mc) >>= \case
CRBroadcastSent {successes, failures} -> Right CRBroadcastSent {successes, failures} ->
sendReply $ "Forwarded to " <> tshow successes <> " contact(s), " <> tshow failures <> " errors" sendReply $ "Forwarded to " <> tshow successes <> " contact(s), " <> tshow failures <> " errors"
r -> putStrLn $ "Error broadcasting message: " <> show r r -> putStrLn $ "Error broadcasting message: " <> show r
| otherwise -> | otherwise ->

View file

@ -2,20 +2,23 @@
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-} {-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
module Server where module Server where
import Control.Monad import Control.Monad
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.Reader import Control.Monad.Reader
import Data.Aeson (FromJSON, ToJSON) import Data.Aeson (FromJSON, ToJSON (..))
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.TH as JQ
import Data.Bifunctor (first)
import Data.Text (Text) import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import GHC.Generics (Generic) import GHC.Generics (Generic)
@ -26,7 +29,7 @@ import Simplex.Chat.Controller
import Simplex.Chat.Core import Simplex.Chat.Core
import Simplex.Chat.Library.Commands import Simplex.Chat.Library.Commands
import Simplex.Chat.Options 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.Transport.Server (runLocalTCPServer)
import Simplex.Messaging.Util (raceAny_) import Simplex.Messaging.Util (raceAny_)
import UnliftIO.Exception import UnliftIO.Exception
@ -35,13 +38,32 @@ import UnliftIO.STM
data ChatSrvRequest = ChatSrvRequest {corrId :: Text, cmd :: Text} data ChatSrvRequest = ChatSrvRequest {corrId :: Text, cmd :: Text}
deriving (Generic, FromJSON) 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) data AChatSrvResponse = forall r. ToJSON (ChatSrvResponse r) => ACR (ChatSrvResponse r)
$(pure []) $(pure [])
instance ToJSON r => ToJSON (ChatSrvResponse r) where instance ToJSON (CSRBody r) => ToJSON (ChatSrvResponse r) where
toEncoding = $(JQ.mkToEncoding defaultJSON ''ChatSrvResponse) toEncoding = $(JQ.mkToEncoding defaultJSON ''ChatSrvResponse)
toJSON = $(JQ.mkToJSON defaultJSON ''ChatSrvResponse) toJSON = $(JQ.mkToJSON defaultJSON ''ChatSrvResponse)
@ -91,8 +113,8 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do
>>= processCommand >>= processCommand
>>= atomically . writeTBQueue sndQ . ACR >>= atomically . writeTBQueue sndQ . ACR
output ChatClient {sndQ} = forever $ do output ChatClient {sndQ} = forever $ do
(_, resp) <- atomically . readTBQueue $ outputQ cc (_, r) <- atomically . readTBQueue $ outputQ cc
atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId = Nothing, resp} atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId = Nothing, resp = CSRBody r}
receive ws ChatClient {rcvQ, sndQ} = forever $ do receive ws ChatClient {rcvQ, sndQ} = forever $ do
s <- WS.receiveData ws s <- WS.receiveData ws
case J.decodeStrict' s of case J.decodeStrict' s of
@ -103,11 +125,9 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do
Left e -> sendError (Just corrId) e Left e -> sendError (Just corrId) e
Nothing -> sendError Nothing "invalid request" Nothing -> sendError Nothing "invalid request"
where 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) = processCommand (corrId, cmd) =
runReaderT (runExceptT $ processChatCommand cmd) cc >>= \case response <$> runReaderT (runExceptT $ processChatCommand cmd) cc
Right resp -> response resp
Left e -> response $ CRChatCmdError Nothing e
where where
response resp = pure ChatSrvResponse {corrId = Just corrId, resp} response r = ChatSrvResponse {corrId = Just corrId, resp = CSRBody r}
clientDisconnected _ = pure () clientDisconnected _ = pure ()

View file

@ -63,8 +63,16 @@ data DirectoryEvent
| DELogChatResponse Text | DELogChatResponse Text
deriving (Show) deriving (Show)
crDirectoryEvent :: ChatEvent -> Maybe DirectoryEvent crDirectoryEvent :: Either ChatError ChatEvent -> Maybe DirectoryEvent
crDirectoryEvent = \case 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 CEvtContactConnected {contact} -> Just $ DEContactConnected contact
CEvtReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} CEvtReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole}
CEvtUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember CEvtUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember
@ -92,10 +100,6 @@ crDirectoryEvent = \case
ciId = chatItemId' ci ciId = chatItemId' ci
err = ADC SDRUser DCUnknownCommand err = ADC SDRUser DCUnknownCommand
CEvtMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage 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) CEvtChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors)
_ -> Nothing _ -> Nothing
where where

View file

@ -60,7 +60,7 @@ import Simplex.Chat.Terminal (terminalChatConfig)
import Simplex.Chat.Terminal.Main (simplexChatCLI') import Simplex.Chat.Terminal.Main (simplexChatCLI')
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Shared 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.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..))
import Simplex.Messaging.Agent.Store.Common (withTransaction) import Simplex.Messaging.Agent.Store.Common (withTransaction)
import Simplex.Messaging.Agent.Protocol (SConnectionMode (..), sameConnReqContact, sameShortLinkContact) 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) 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} 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 = directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName, ownersGroup, searchResults} env@ServiceState {searchRequests} user@User {userId} cc event =
forM_ (crDirectoryEvent event) $ \case forM_ (crDirectoryEvent event) $ \case
DEContactConnected ct -> deContactConnected ct DEContactConnected ct -> deContactConnected ct
@ -249,7 +249,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)]) getGroups_ :: Maybe Text -> IO (Maybe [(GroupInfo, GroupSummary)])
getGroups_ search_ = getGroups_ search_ =
sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case sendChatCmd cc (APIListGroups userId Nothing $ T.unpack <$> search_) >>= \case
CRGroupsList {groups} -> pure $ Just groups Right CRGroupsList {groups} -> pure $ Just groups
_ -> pure Nothing _ -> pure Nothing
getDuplicateGroup :: GroupInfo -> IO (Maybe DuplicateGroup) getDuplicateGroup :: GroupInfo -> IO (Maybe DuplicateGroup)
@ -281,7 +281,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
void $ addGroupReg st ct g GRSProposed void $ addGroupReg st ct g GRSProposed
r <- sendChatCmd cc $ APIJoinGroup groupId MFNone r <- sendChatCmd cc $ APIJoinGroup groupId MFNone
sendMessage cc ct $ case r of 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!" _ -> "Error joining group " <> displayName <> ", please re-send the invitation!"
deContactConnected :: Contact -> IO () deContactConnected :: Contact -> IO ()
@ -337,7 +337,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
$>>= \mId -> resp <$> sendChatCmd cc (APIGroupMemberInfo dbGroupId mId) $>>= \mId -> resp <$> sendChatCmd cc (APIGroupMemberInfo dbGroupId mId)
where where
resp = \case resp = \case
CRGroupMemberInfo {member} -> Just member Right CRGroupMemberInfo {member} -> Just member
_ -> Nothing _ -> Nothing
deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO ()
@ -349,7 +349,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g
notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…"
sendChatCmd cc (APICreateGroupLink groupId GRMember False) >>= \case sendChatCmd cc (APICreateGroupLink groupId GRMember False) >>= \case
CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> do Right CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> do
setGroupStatus st gr GRSPendingUpdate setGroupStatus st gr GRSPendingUpdate
notifyOwner notifyOwner
gr gr
@ -357,7 +357,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
\Please add it to the group welcome message.\n\ \Please add it to the group welcome message.\n\
\For example, add:" \For example, add:"
notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact gLink) 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." 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." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group."
CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined" CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined"
@ -446,7 +446,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
where where
profileUpdate = \case profileUpdate = \case
CRGroupLink {connLinkContact = CCLink cr sl_} -> Right CRGroupLink {connLinkContact = CCLink cr sl_} ->
let hadLinkBefore = profileHasGroupLink fromGroup let hadLinkBefore = profileHasGroupLink fromGroup
hasLinkNow = profileHasGroupLink toGroup hasLinkNow = profileHasGroupLink toGroup
profileHasGroupLink GroupInfo {groupProfile = gp} = 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_ let role = if useMemberFilter image (makeObserver a) then GRObserver else maybe GRMember (\GroupLinkInfo {memberRole} -> memberRole) gli_
gmId = groupMemberId' m gmId = groupMemberId' m
sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case sendChatCmd cc (APIAcceptMember groupId gmId role) >>= \case
CRJoinedGroupMember {} -> do Right CRJoinedGroupMember {} -> do
atomically $ TM.delete gmId $ pendingCaptchas env atomically $ TM.delete gmId $ pendingCaptchas env
logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g logInfo $ "Member " <> viewName displayName <> " accepted, group " <> tshow groupId <> ":" <> viewGroupName g
r -> logError $ "unexpected accept member response: " <> tshow r r -> logError $ "unexpected accept member response: " <> tshow r
@ -528,7 +528,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let gmId = groupMemberId' m let gmId = groupMemberId' m
sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice] sendComposedMessages cc (SRGroup groupId $ Just gmId) [MCText rjctNotice]
sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case sendChatCmd cc (APIRemoveMembers groupId [gmId] False) >>= \case
CRUserDeletedMembers _ _ (_ : _) _ -> do Right (CRUserDeletedMembers _ _ (_ : _) _) -> do
atomically $ TM.delete gmId $ pendingCaptchas env atomically $ TM.delete gmId $ pendingCaptchas env
logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g logInfo $ "Member " <> viewName displayName <> " rejected, group " <> tshow groupId <> ":" <> viewGroupName g
r -> logError $ "unexpected remove member response: " <> tshow r r -> logError $ "unexpected remove member response: " <> tshow r
@ -891,18 +891,21 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let groupRef = groupReference' groupId gName let groupRef = groupReference' groupId gName
withGroupAndReg sendReply groupId gName $ \_ _ -> withGroupAndReg sendReply groupId gName $ \_ _ ->
sendChatCmd cc (APIGetGroupLink groupId) >>= \case sendChatCmd cc (APIGetGroupLink groupId) >>= \case
CRGroupLink {connLinkContact = CCLink cReq _, memberRole} -> Right CRGroupLink {connLinkContact = CCLink cReq _, memberRole} ->
sendReply $ T.unlines sendReply $ T.unlines
[ "The link to join the group " <> groupRef <> ":", [ "The link to join the group " <> groupRef <> ":",
strEncodeTxt $ simplexChatContact cReq, strEncodeTxt $ simplexChatContact cReq,
"New member role: " <> strEncodeTxt memberRole "New member role: " <> strEncodeTxt memberRole
] ]
CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) -> Left (ChatErrorStore (SEGroupLinkNotFound _)) ->
sendReply $ "The group " <> groupRef <> " has no public link." sendReply $ "The group " <> groupRef <> " has no public link."
r -> do Right r -> do
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone 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 sendReply $ "Unexpected error:\n" <> resp
DCSendToGroupOwner groupId gName msg -> do DCSendToGroupOwner groupId gName msg -> do
let groupRef = groupReference' groupId gName 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 -> GroupReg -> (Either Text () -> IO a) -> IO a
inviteToOwnersGroup KnownGroup {groupId = ogId} GroupReg {dbContactId = ctId} cont = inviteToOwnersGroup KnownGroup {groupId = ogId} GroupReg {dbContactId = ctId} cont =
sendChatCmd cc (APIListMembers ogId) >>= \case sendChatCmd cc (APIListMembers ogId) >>= \case
CRGroupMembers _ (Group _ ms) Right (CRGroupMembers _ (Group _ ms))
| alreadyMember ms -> cont $ Left "Owner is already a member of owners' group" | alreadyMember ms -> cont $ Left "Owner is already a member of owners' group"
| otherwise -> do | otherwise -> do
sendChatCmd cc (APIAddMember ogId ctId GRMember) >>= \case sendChatCmd cc (APIAddMember ogId ctId GRMember) >>= \case
CRSentGroupInvitation {} -> do Right CRSentGroupInvitation {} -> do
printLog cc CLLInfo $ "invited contact ID " <> show ctId <> " to owners' group" printLog cc CLLInfo $ "invited contact ID " <> show ctId <> " to owners' group"
cont $ Right () cont $ Right ()
r -> contErr r r -> contErr r
@ -969,10 +972,13 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
deSuperUserCommand ct ciId cmd deSuperUserCommand ct ciId cmd
| knownContact ct `elem` superUsers = case cmd of | knownContact ct `elem` superUsers = case cmd of
DCExecuteCommand cmdStr -> DCExecuteCommand cmdStr ->
sendChatCmdStr cc cmdStr >>= \r -> do sendChatCmdStr cc cmdStr >>= \case
Right r -> do
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
sendReply $ T.pack $ serializeChatResponse (Nothing, Just user) ts tz Nothing r 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 DCCommandError tag -> sendReply $ "Command error: " <> tshow tag
| otherwise = sendReply "You are not allowed to use this command" | otherwise = sendReply "You are not allowed to use this command"
where where
@ -1045,7 +1051,7 @@ setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe
setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole)
where where
resp = \case resp = \case
CRGroupLink _ _ (CCLink gLink _) _ -> Just gLink Right (CRGroupLink _ _ (CCLink gLink _) _) -> Just gLink
_ -> Nothing _ -> Nothing
unexpectedError :: Text -> Text unexpectedError :: Text -> Text

View file

@ -467,6 +467,8 @@ executable simplex-directory-service
, text >=1.2.4.0 && <1.3 , text >=1.2.4.0 && <1.3
test-suite simplex-chat-test test-suite simplex-chat-test
if flag(swift)
cpp-options: -DswiftJSON
if flag(client_library) if flag(client_library)
buildable: False buildable: False
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0

View file

@ -35,10 +35,10 @@ chatBotRepl welcome answer _user cc = do
race_ (forever $ void getLine) . forever $ do race_ (forever $ void getLine) . forever $ do
(_, event) <- atomically . readTBQueue $ outputQ cc (_, event) <- atomically . readTBQueue $ outputQ cc
case event of case event of
CEvtContactConnected _ contact _ -> do Right (CEvtContactConnected _ contact _) -> do
contactConnected contact contactConnected contact
void $ sendMessage cc contact $ T.pack welcome void $ sendMessage cc contact $ T.pack welcome
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 = T.unpack $ ciContentToText mc let msg = T.unpack $ ciContentToText mc
void $ sendMessage cc contact . T.pack =<< answer contact msg void $ sendMessage cc contact . T.pack =<< answer contact msg
_ -> pure () _ -> pure ()
@ -51,12 +51,12 @@ initializeBotAddress = initializeBotAddress' True
initializeBotAddress' :: Bool -> ChatController -> IO () initializeBotAddress' :: Bool -> ChatController -> IO ()
initializeBotAddress' logAddress cc = do initializeBotAddress' logAddress cc = do
sendChatCmd cc ShowMyAddress >>= \case sendChatCmd cc ShowMyAddress >>= \case
CRUserContactLink _ UserContactLink {connLinkContact} -> showBotAddress connLinkContact Right (CRUserContactLink _ UserContactLink {connLinkContact}) -> showBotAddress connLinkContact
CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do Left (ChatErrorStore SEUserContactLinkNotFound) -> do
when logAddress $ putStrLn "No bot address, creating..." when logAddress $ putStrLn "No bot address, creating..."
-- TODO [short links] create short link by default -- TODO [short links] create short link by default
sendChatCmd cc (CreateMyAddress False) >>= \case sendChatCmd cc (CreateMyAddress False) >>= \case
CRUserContactLinkCreated _ ccLink -> showBotAddress ccLink Right (CRUserContactLinkCreated _ ccLink) -> showBotAddress ccLink
_ -> putStrLn "can't create bot address" >> exitFailure _ -> putStrLn "can't create bot address" >> exitFailure
_ -> putStrLn "unexpected response" >> exitFailure _ -> putStrLn "unexpected response" >> exitFailure
where where
@ -84,14 +84,14 @@ sendComposedMessages_ :: ChatController -> SendRef -> NonEmpty (Maybe ChatItemId
sendComposedMessages_ cc sendRef qmcs = do sendComposedMessages_ cc sendRef qmcs = do
let cms = L.map (\(qiId, mc) -> ComposedMessage {fileSource = Nothing, quotedItemId = qiId, msgContent = mc, mentions = M.empty}) qmcs let cms = L.map (\(qiId, mc) -> ComposedMessage {fileSource = Nothing, quotedItemId = qiId, msgContent = mc, mentions = M.empty}) qmcs
sendChatCmd cc (APISendMessages sendRef False Nothing cms) >>= \case sendChatCmd cc (APISendMessages sendRef False Nothing cms) >>= \case
CRNewChatItems {} -> printLog cc CLLInfo $ "sent " <> show (length cms) <> " messages to " <> show sendRef Right (CRNewChatItems {}) -> printLog cc CLLInfo $ "sent " <> show (length cms) <> " messages to " <> show sendRef
r -> putStrLn $ "unexpected send message response: " <> show r r -> putStrLn $ "unexpected send message response: " <> show r
deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () deleteMessage :: ChatController -> Contact -> ChatItemId -> IO ()
deleteMessage cc ct chatItemId = do deleteMessage cc ct chatItemId = do
let cmd = APIDeleteChatItem (contactRef ct) [chatItemId] CIDMInternal let cmd = APIDeleteChatItem (contactRef ct) [chatItemId] CIDMInternal
sendChatCmd cc cmd >>= \case sendChatCmd cc cmd >>= \case
CRChatItemsDeleted {} -> printLog cc CLLInfo $ "deleted message(s) from " <> contactInfo ct Right (CRChatItemsDeleted {}) -> printLog cc CLLInfo $ "deleted message(s) from " <> contactInfo ct
r -> putStrLn $ "unexpected delete message response: " <> show r r -> putStrLn $ "unexpected delete message response: " <> show r
contactRef :: Contact -> ChatRef contactRef :: Contact -> ChatRef

View file

@ -172,10 +172,10 @@ data ChatHooks = ChatHooks
{ -- preCmdHook can be used to process or modify the commands before they are processed. { -- preCmdHook can be used to process or modify the commands before they are processed.
-- This hook should be used to process CustomChatCommand. -- This hook should be used to process CustomChatCommand.
-- if this hook returns ChatResponse, the command processing will be skipped. -- if this hook returns ChatResponse, the command processing will be skipped.
preCmdHook :: Maybe (ChatController -> ChatCommand -> IO (Either ChatResponse ChatCommand)), preCmdHook :: Maybe (ChatController -> ChatCommand -> IO (Either (Either ChatError ChatResponse) ChatCommand)),
-- eventHook can be used to additionally process or modify events, -- eventHook can be used to additionally process or modify events,
-- it is called before the event is sent to the user (or to the UI). -- it is called before the event is sent to the user (or to the UI).
eventHook :: Maybe (ChatController -> ChatEvent -> IO ChatEvent), eventHook :: Maybe (ChatController -> Either ChatError ChatEvent -> IO (Either ChatError ChatEvent)),
-- acceptMember hook can be used to accept or reject member connecting via group link without API calls -- acceptMember hook can be used to accept or reject member connecting via group link without API calls
acceptMember :: Maybe (GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole))) acceptMember :: Maybe (GroupInfo -> GroupLinkInfo -> Profile -> IO (Either GroupRejectionReason (GroupAcceptance, GroupMemberRole)))
} }
@ -223,7 +223,7 @@ data ChatController = ChatController
random :: TVar ChaChaDRG, random :: TVar ChaChaDRG,
eventSeq :: TVar Int, eventSeq :: TVar Int,
inputQ :: TBQueue String, inputQ :: TBQueue String,
outputQ :: TBQueue (Maybe RemoteHostId, ChatEvent), outputQ :: TBQueue (Maybe RemoteHostId, Either ChatError ChatEvent),
connNetworkStatuses :: TMap AgentConnId NetworkStatus, connNetworkStatuses :: TMap AgentConnId NetworkStatus,
subscriptionMode :: TVar SubscriptionMode, subscriptionMode :: TVar SubscriptionMode,
chatLock :: Lock, chatLock :: Lock,
@ -731,7 +731,6 @@ data ChatResponse
| CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]}
| CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo}
| CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo} | CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo}
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
| CRAppSettings {appSettings :: AppSettings} | CRAppSettings {appSettings :: AppSettings}
| CRCustomChatResponse {user_ :: Maybe User, response :: Text} | CRCustomChatResponse {user_ :: Maybe User, response :: Text}
deriving (Show) deriving (Show)
@ -839,8 +838,7 @@ data ChatEvent
| CEvtAgentConnsDeleted {agentConnIds :: NonEmpty AgentConnId} | CEvtAgentConnsDeleted {agentConnIds :: NonEmpty AgentConnId}
| CEvtAgentUserDeleted {agentUserId :: Int64} | CEvtAgentUserDeleted {agentUserId :: Int64}
| CEvtMessageError {user :: User, severity :: Text, errorMessage :: Text} | CEvtMessageError {user :: User, severity :: Text, errorMessage :: Text}
| CEvtChatError {user_ :: Maybe User, chatError :: ChatError} | CEvtChatErrors {chatErrors :: [ChatError]}
| CEvtChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]}
| CEvtTimedAction {action :: String, durationMilliseconds :: Int64} | CEvtTimedAction {action :: String, durationMilliseconds :: Int64}
| CEvtTerminalEvent TerminalEvent | CEvtTerminalEvent TerminalEvent
deriving (Show) deriving (Show)
@ -869,7 +867,6 @@ data DeletedRcvQueue = DeletedRcvQueue
} }
deriving (Show) deriving (Show)
-- some of these can only be used as command responses
allowRemoteEvent :: ChatEvent -> Bool allowRemoteEvent :: ChatEvent -> Bool
allowRemoteEvent = \case allowRemoteEvent = \case
CEvtChatSuspended -> False CEvtChatSuspended -> False
@ -893,8 +890,7 @@ logEventToFile = \case
CEvtAgentRcvQueuesDeleted {} -> True CEvtAgentRcvQueuesDeleted {} -> True
CEvtAgentConnsDeleted {} -> True CEvtAgentConnsDeleted {} -> True
CEvtAgentUserDeleted {} -> True CEvtAgentUserDeleted {} -> True
-- CEvtChatCmdError {} -> True -- TODO this should be separately logged to file -- CRChatCmdError {} -> True -- TODO this should be separately logged to file as command error
CEvtChatError {} -> True
CEvtMessageError {} -> True CEvtMessageError {} -> True
CEvtTerminalEvent te -> case te of CEvtTerminalEvent te -> case te of
TEMemberSubError {} -> True TEMemberSubError {} -> True
@ -1408,7 +1404,7 @@ data RemoteCtrlSession
tls :: TLS, tls :: TLS,
rcsSession :: RCCtrlSession, rcsSession :: RCCtrlSession,
http2Server :: Async (), http2Server :: Async (),
remoteOutputQ :: TBQueue ChatEvent remoteOutputQ :: TBQueue (Either ChatError ChatEvent)
} }
data RemoteCtrlSessionState data RemoteCtrlSessionState
@ -1507,11 +1503,17 @@ mkStoreError :: SomeException -> StoreError
mkStoreError = SEInternalError . show mkStoreError = SEInternalError . show
{-# INLINE mkStoreError #-} {-# INLINE mkStoreError #-}
chatCmdError :: Maybe User -> String -> ChatResponse throwCmdError :: String -> CM a
chatCmdError user = CRChatCmdError user . ChatError . CECommandError throwCmdError = throwError . ChatError . CECommandError
{-# INLINE throwCmdError #-}
chatCmdError :: String -> Either ChatError ChatResponse
chatCmdError = Left . ChatError . CECommandError
{-# INLINE chatCmdError #-}
throwChatError :: ChatErrorType -> CM a throwChatError :: ChatErrorType -> CM a
throwChatError = throwError . ChatError throwChatError = throwError . ChatError
{-# INLINE throwChatError #-}
toViewTE :: TerminalEvent -> CM () toViewTE :: TerminalEvent -> CM ()
toViewTE = toView . CEvtTerminalEvent toViewTE = toView . CEvtTerminalEvent
@ -1523,7 +1525,19 @@ toView = lift . toView'
{-# INLINE toView #-} {-# INLINE toView #-}
toView' :: ChatEvent -> CM' () toView' :: ChatEvent -> CM' ()
toView' ev = do toView' = toView_ . Right
{-# INLINE toView' #-}
eToView :: ChatError -> CM ()
eToView = lift . eToView'
{-# INLINE eToView #-}
eToView' :: ChatError -> CM' ()
eToView' = toView_ . Left
{-# INLINE eToView' #-}
toView_ :: Either ChatError ChatEvent -> CM' ()
toView_ ev = do
cc@ChatController {outputQ = localQ, remoteCtrlSession = session, config = ChatConfig {chatHooks}} <- ask cc@ChatController {outputQ = localQ, remoteCtrlSession = session, config = ChatConfig {chatHooks}} <- ask
event <- case eventHook chatHooks of event <- case eventHook chatHooks of
Just hook -> liftIO $ hook cc ev Just hook -> liftIO $ hook cc ev
@ -1531,7 +1545,7 @@ toView' ev = do
atomically $ atomically $
readTVar session >>= \case readTVar session >>= \case
Just (_, RCSessionConnected {remoteOutputQ}) Just (_, RCSessionConnected {remoteOutputQ})
| allowRemoteEvent event -> writeTBQueue remoteOutputQ event | either (const True) allowRemoteEvent event -> writeTBQueue remoteOutputQ event
-- TODO potentially, it should hold some events while connecting -- TODO potentially, it should hold some events while connecting
_ -> writeTBQueue localQ (Nothing, event) _ -> writeTBQueue localQ (Nothing, event)

View file

@ -8,6 +8,7 @@ module Simplex.Chat.Core
runSimplexChat, runSimplexChat,
sendChatCmdStr, sendChatCmdStr,
sendChatCmd, sendChatCmd,
printResponseEvent,
) )
where where
@ -23,9 +24,10 @@ import Simplex.Chat
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Library.Commands import Simplex.Chat.Library.Commands
import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..))
import Simplex.Chat.Remote.Types (RemoteHostId)
import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Profiles
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.View (serializeChatResponse) import Simplex.Chat.View (ChatResponseEvent, serializeChatError, serializeChatResponse)
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..)) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..))
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction)
import System.Exit (exitFailure) import System.Exit (exitFailure)
@ -62,10 +64,10 @@ runSimplexChat ChatOpts {maintenance} u cc chat
a2 <- async $ chat u cc a2 <- async $ chat u cc
waitEither_ a1 a2 waitEither_ a1 a2
sendChatCmdStr :: ChatController -> String -> IO ChatResponse sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse)
sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc
sendChatCmd :: ChatController -> ChatCommand -> IO ChatResponse sendChatCmd :: ChatController -> ChatCommand -> IO (Either ChatError ChatResponse)
sendChatCmd cc cmd = runReaderT (execChatCommand' cmd) cc sendChatCmd cc cmd = runReaderT (execChatCommand' cmd) cc
getSelectActiveUser :: DBStore -> IO (Maybe User) getSelectActiveUser :: DBStore -> IO (Maybe User)
@ -107,12 +109,17 @@ createActiveUser cc = do
displayName <- T.pack <$> getWithPrompt "display name" displayName <- T.pack <$> getWithPrompt "display name"
let profile = Just Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} let profile = Just Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing}
execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) `runReaderT` cc >>= \case execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) `runReaderT` cc >>= \case
CRActiveUser user -> pure user Right (CRActiveUser user) -> pure user
r -> do r -> printResponseEvent (Nothing, Nothing) (config cc) r >> loop
printResponseEvent :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Either ChatError r -> IO ()
printResponseEvent hu cfg = \case
Right r -> do
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
putStrLn $ serializeChatResponse (Nothing, Nothing) ts tz Nothing r putStrLn $ serializeChatResponse hu cfg ts tz (fst hu) r
loop Left e -> do
putStrLn $ serializeChatError True cfg e
getWithPrompt :: String -> IO String getWithPrompt :: String -> IO String
getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine getWithPrompt s = putStr (s <> ": ") >> hFlush stdout >> getLine

View file

@ -225,7 +225,7 @@ startReceiveUserFiles :: User -> CM ()
startReceiveUserFiles user = do startReceiveUserFiles user = do
filesToReceive <- withStore' (`getRcvFilesToReceive` user) filesToReceive <- withStore' (`getRcvFilesToReceive` user)
forM_ filesToReceive $ \ft -> forM_ filesToReceive $ \ft ->
flip catchChatError (toView . CEvtChatError (Just user)) $ flip catchChatError eToView $
toView =<< receiveFileEvt' user ft False Nothing Nothing toView =<< receiveFileEvt' user ft False Nothing Nothing
restoreCalls :: CM' () restoreCalls :: CM' ()
@ -267,32 +267,28 @@ useServers as opDomains uss =
xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss
in (smp', xftp') in (smp', xftp')
execChatCommand :: Maybe RemoteHostId -> ByteString -> CM' ChatResponse execChatCommand :: Maybe RemoteHostId -> ByteString -> CM' (Either ChatError ChatResponse)
execChatCommand rh s = do execChatCommand rh s =
u <- readTVarIO =<< asks currentUser
case parseChatCommand s of case parseChatCommand s of
Left e -> pure $ chatCmdError u e Left e -> pure $ chatCmdError e
Right cmd -> case rh of Right cmd -> case rh of
Just rhId Just rhId
| allowRemoteCommand cmd -> execRemoteCommand u rhId cmd s | allowRemoteCommand cmd -> execRemoteCommand rhId cmd s
| otherwise -> pure $ CRChatCmdError u $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand | otherwise -> pure $ Left $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand
_ -> do _ -> do
cc@ChatController {config = ChatConfig {chatHooks}} <- ask cc@ChatController {config = ChatConfig {chatHooks}} <- ask
case preCmdHook chatHooks of case preCmdHook chatHooks of
Just hook -> liftIO (hook cc cmd) >>= either pure (execChatCommand_ u) Just hook -> liftIO (hook cc cmd) >>= either pure execChatCommand'
Nothing -> execChatCommand_ u cmd Nothing -> execChatCommand' cmd
execChatCommand' :: ChatCommand -> CM' ChatResponse execChatCommand' :: ChatCommand -> CM' (Either ChatError ChatResponse)
execChatCommand' cmd = asks currentUser >>= readTVarIO >>= (`execChatCommand_` cmd) execChatCommand' cmd = handleCommandError $ processChatCommand cmd
execChatCommand_ :: Maybe User -> ChatCommand -> CM' ChatResponse execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> CM' (Either ChatError ChatResponse)
execChatCommand_ u cmd = handleCommandError u $ processChatCommand cmd execRemoteCommand rhId cmd s = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s
execRemoteCommand :: Maybe User -> RemoteHostId -> ChatCommand -> ByteString -> CM' ChatResponse handleCommandError :: CM ChatResponse -> CM' (Either ChatError ChatResponse)
execRemoteCommand u rhId cmd s = handleCommandError u $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s handleCommandError a = runExceptT a `E.catches` ioErrors
handleCommandError :: Maybe User -> CM ChatResponse -> CM' ChatResponse
handleCommandError u a = either (CRChatCmdError u) id <$> (runExceptT a `E.catches` ioErrors)
where where
ioErrors = ioErrors =
[ E.Handler $ \(e :: ExitCode) -> E.throwIO e, [ E.Handler $ \(e :: ExitCode) -> E.throwIO e,
@ -502,7 +498,7 @@ processChatCommand' vr = \case
pure $ CRChatTags user tags pure $ CRChatTags user tags
APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do
(errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query)
unless (null errs) $ toView $ CEvtChatErrors (Just user) (map ChatErrorStore errs) unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs)
pure $ CRApiChats user previews pure $ CRApiChats user previews
APIGetChat (ChatRef cType cId) contentFilter pagination search -> withUser $ \user -> case cType of APIGetChat (ChatRef cType cId) contentFilter pagination search -> withUser $ \user -> case cType of
-- TODO optimize queries calculating ChatStats, currently they're disabled -- TODO optimize queries calculating ChatStats, currently they're disabled
@ -517,8 +513,8 @@ processChatCommand' vr = \case
when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported" when (isJust contentFilter) $ throwChatError $ CECommandError "content filter not supported"
(localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search) (localChat, navInfo) <- withFastStore (\db -> getLocalChat db user cId pagination search)
pure $ CRApiChat user (AChat SCTLocal localChat) navInfo pure $ CRApiChat user (AChat SCTLocal localChat) navInfo
CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" CTContactRequest -> throwCmdError "not implemented"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> throwCmdError "not supported"
APIGetChatItems pagination search -> withUser $ \user -> do APIGetChatItems pagination search -> withUser $ \user -> do
chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search
pure $ CRChatItems user Nothing chatItems pure $ CRChatItems user Nothing chatItems
@ -553,14 +549,14 @@ processChatCommand' vr = \case
APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do
_ <- createChatTag db user emoji text _ <- createChatTag db user emoji text
CRChatTags user <$> getUserChatTags db user CRChatTags user <$> getUserChatTags db user
APISetChatTags (ChatRef cType chatId) tagIds -> withUser $ \user -> withFastStore' $ \db -> case cType of APISetChatTags (ChatRef cType chatId) tagIds -> withUser $ \user -> case cType of
CTDirect -> do CTDirect -> withFastStore' $ \db -> do
updateDirectChatTags db chatId (maybe [] L.toList tagIds) updateDirectChatTags db chatId (maybe [] L.toList tagIds)
CRTagsUpdated user <$> getUserChatTags db user <*> getDirectChatTags db chatId CRTagsUpdated user <$> getUserChatTags db user <*> getDirectChatTags db chatId
CTGroup -> do CTGroup -> withFastStore' $ \db -> do
updateGroupChatTags db chatId (maybe [] L.toList tagIds) updateGroupChatTags db chatId (maybe [] L.toList tagIds)
CRTagsUpdated user <$> getUserChatTags db user <*> getGroupChatTags db chatId CRTagsUpdated user <$> getUserChatTags db user <*> getGroupChatTags db chatId
_ -> pure $ chatCmdError (Just user) "not supported" _ -> throwCmdError "not supported"
APIDeleteChatTag tagId -> withUser $ \user -> do APIDeleteChatTag tagId -> withUser $ \user -> do
withFastStore' $ \db -> deleteChatTag db user tagId withFastStore' $ \db -> deleteChatTag db user tagId
ok user ok user
@ -622,7 +618,7 @@ processChatCommand' vr = \case
assertUserGroupRole gInfo GRAuthor assertUserGroupRole gInfo GRAuthor
let (_, ft_) = msgContentTexts mc let (_, ft_) = msgContentTexts mc
if prohibitedSimplexLinks gInfo membership ft_ if prohibitedSimplexLinks gInfo membership ft_
then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) then throwCmdError ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks))
else do else do
cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId
case cci of case cci of
@ -660,8 +656,8 @@ processChatCommand' vr = \case
ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) True ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) True
pure $ CRChatItemUpdated user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci') pure $ CRChatItemUpdated user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci')
_ -> throwChatError CEInvalidChatItemUpdate _ -> throwChatError CEInvalidChatItemUpdate
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> throwCmdError "not supported"
APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of
CTDirect -> withContactLock "deleteChatItem" chatId $ do CTDirect -> withContactLock "deleteChatItem" chatId $ do
(ct, items) <- getCommandDirectChatItems user chatId itemIds (ct, items) <- getCommandDirectChatItems user chatId itemIds
@ -697,8 +693,8 @@ processChatCommand' vr = \case
CTLocal -> do CTLocal -> do
(nf, items) <- getCommandLocalChatItems user chatId itemIds (nf, items) <- getCommandLocalChatItems user chatId itemIds
deleteLocalCIs user nf items True False deleteLocalCIs user nf items True False
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> throwCmdError "not supported"
where where
assertDeletable :: forall c. ChatTypeI c => [CChatItem c] -> CM () assertDeletable :: forall c. ChatTypeI c => [CChatItem c] -> CM ()
assertDeletable items = do assertDeletable items = do
@ -781,9 +777,9 @@ processChatCommand' vr = \case
r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction
pure $ CRChatItemReaction user add r pure $ CRChatItemReaction user add r
_ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID"
CTLocal -> pure $ chatCmdError (Just user) "not supported" CTLocal -> throwCmdError "not supported"
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> throwCmdError "not supported"
where where
checkReactionAllowed rs = do checkReactionAllowed rs = do
when ((reaction `elem` rs) == add) $ when ((reaction `elem` rs) == add) $
@ -799,8 +795,8 @@ processChatCommand' vr = \case
CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds CTDirect -> planForward user . snd =<< getCommandDirectChatItems user fromChatId itemIds
CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds CTGroup -> planForward user . snd =<< getCommandGroupChatItems user fromChatId itemIds
CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds CTLocal -> planForward user . snd =<< getCommandLocalChatItems user fromChatId itemIds
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> throwCmdError "not supported"
where where
planForward :: User -> [CChatItem c] -> CM ChatResponse planForward :: User -> [CChatItem c] -> CM ChatResponse
planForward user items = do planForward user items = do
@ -863,8 +859,8 @@ processChatCommand' vr = \case
Just cmrs' -> Just cmrs' ->
createNoteFolderContentItems user toChatId cmrs' createNoteFolderContentItems user toChatId cmrs'
Nothing -> pure $ CRNewChatItems user [] Nothing -> pure $ CRNewChatItems user []
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> throwCmdError "not supported"
where where
prepareForward :: User -> CM [ComposedMessageReq] prepareForward :: User -> CM [ComposedMessageReq]
prepareForward user = case fromCType of prepareForward user = case fromCType of
@ -1004,8 +1000,8 @@ processChatCommand' vr = \case
user <- withFastStore $ \db -> getUserByNoteFolderId db chatId user <- withFastStore $ \db -> getUserByNoteFolderId db chatId
withFastStore' $ \db -> updateLocalChatItemsRead db user chatId withFastStore' $ \db -> updateLocalChatItemsRead db user chatId
ok user ok user
CTContactRequest -> pure $ chatCmdError Nothing "not supported" CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> pure $ chatCmdError Nothing "not supported" CTContactConnection -> throwCmdError "not supported"
APIChatItemsRead chatRef@(ChatRef cType chatId) itemIds -> withUser $ \_ -> case cType of APIChatItemsRead chatRef@(ChatRef cType chatId) itemIds -> withUser $ \_ -> case cType of
CTDirect -> do CTDirect -> do
user <- withFastStore $ \db -> getUserByContactId db chatId user <- withFastStore $ \db -> getUserByContactId db chatId
@ -1021,9 +1017,9 @@ processChatCommand' vr = \case
setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime
forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt
ok user ok user
CTLocal -> pure $ chatCmdError Nothing "not supported" CTLocal -> throwCmdError "not supported"
CTContactRequest -> pure $ chatCmdError Nothing "not supported" CTContactRequest -> throwCmdError "not supported"
CTContactConnection -> pure $ chatCmdError Nothing "not supported" CTContactConnection -> throwCmdError "not supported"
APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of
CTDirect -> do CTDirect -> do
withFastStore $ \db -> do withFastStore $ \db -> do
@ -1040,7 +1036,7 @@ processChatCommand' vr = \case
nf <- getNoteFolder db user chatId nf <- getNoteFolder db user chatId
liftIO $ updateNoteFolderUnreadChat db user nf unreadChat liftIO $ updateNoteFolderUnreadChat db user nf unreadChat
ok user ok user
_ -> pure $ chatCmdError (Just user) "not supported" _ -> throwCmdError "not supported"
APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do CTDirect -> do
ct <- withFastStore $ \db -> getContact db vr user chatId ct <- withFastStore $ \db -> getContact db vr user chatId
@ -1074,10 +1070,10 @@ processChatCommand' vr = \case
let doSendDel = contactReady ct && contactActive ct && notify let doSendDel = contactReady ct && contactActive ct && notify
when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ())
contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct)
deleteAgentConnectionsAsync' user contactConnIds doSendDel deleteAgentConnectionsAsync' contactConnIds doSendDel
CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do
conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId
deleteAgentConnectionAsync user acId deleteAgentConnectionAsync acId
withFastStore' $ \db -> deletePendingContactConnection db userId chatId withFastStore' $ \db -> deletePendingContactConnection db userId chatId
pure $ CRContactConnectionDeleted user conn pure $ CRContactConnectionDeleted user conn
CTGroup -> do CTGroup -> do
@ -1100,8 +1096,8 @@ processChatCommand' vr = \case
withFastStore' $ \db -> deleteGroupMembers db user gInfo withFastStore' $ \db -> deleteGroupMembers db user gInfo
withFastStore' $ \db -> deleteGroup db user gInfo withFastStore' $ \db -> deleteGroup db user gInfo
pure $ CRGroupDeletedUser user gInfo pure $ CRGroupDeletedUser user gInfo
CTLocal -> pure $ chatCmdError (Just user) "not supported" CTLocal -> throwCmdError "not supported"
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> throwCmdError "not supported"
APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of
CTDirect -> do CTDirect -> do
ct <- withFastStore $ \db -> getContact db vr user chatId ct <- withFastStore $ \db -> getContact db vr user chatId
@ -1124,8 +1120,8 @@ processChatCommand' vr = \case
withFastStore' $ \db -> deleteNoteFolderFiles db userId nf withFastStore' $ \db -> deleteNoteFolderFiles db userId nf
withFastStore' $ \db -> deleteNoteFolderCIs db user nf withFastStore' $ \db -> deleteNoteFolderCIs db user nf
pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf)
CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> throwCmdError "not supported"
CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> throwCmdError "not supported"
APIAcceptContact incognito connReqId -> withUser $ \_ -> do APIAcceptContact incognito connReqId -> withUser $ \_ -> do
userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId
withUserContactLock "acceptContact" userContactLinkId $ do withUserContactLock "acceptContact" userContactLinkId $ do
@ -1172,7 +1168,7 @@ processChatCommand' vr = \case
forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
ok user ok user
else pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) else throwCmdError ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls))
SendCallInvitation cName callType -> withUser $ \user -> do SendCallInvitation cName callType -> withUser $ \user -> do
contactId <- withFastStore $ \db -> getContactIdByName db user cName contactId <- withFastStore $ \db -> getContactIdByName db user cName
processChatCommand $ APISendCallInvitation contactId callType processChatCommand $ APISendCallInvitation contactId callType
@ -1286,7 +1282,7 @@ processChatCommand' vr = \case
g <- getGroupInfo db vr user chatId g <- getGroupInfo db vr user chatId
liftIO $ setGroupUIThemes db user g uiThemes liftIO $ setGroupUIThemes db user g uiThemes
ok user ok user
_ -> pure $ chatCmdError (Just user) "not supported" _ -> throwCmdError "not supported"
APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken
APIRegisterToken token mode -> withUser $ \_ -> APIRegisterToken token mode -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode) CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode)
@ -1294,10 +1290,10 @@ processChatCommand' vr = \case
APICheckToken token -> withUser $ \_ -> APICheckToken token -> withUser $ \_ ->
CRNtfTokenStatus <$> withAgent (`checkNtfToken` token) CRNtfTokenStatus <$> withAgent (`checkNtfToken` token)
APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_
APIGetNtfConns nonce encNtfInfo -> withUser $ \user -> do APIGetNtfConns nonce encNtfInfo -> withUser $ \_ -> do
ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo
(errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos)) (errs, ntfMsgs) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (getMsgConn db) (L.toList ntfInfos))
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure $ CRNtfConns $ catMaybes ntfMsgs pure $ CRNtfConns $ catMaybes ntfMsgs
where where
getMsgConn :: DB.Connection -> NotificationInfo -> IO (Maybe NtfConn) getMsgConn :: DB.Connection -> NotificationInfo -> IO (Maybe NtfConn)
@ -1408,7 +1404,7 @@ processChatCommand' vr = \case
oldTTL = fromMaybe globalTTL oldTTL_ oldTTL = fromMaybe globalTTL oldTTL_
when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do when (newTTL > 0 && (newTTL < oldTTL || oldTTL == 0)) $ do
lift $ setExpireCIFlag user False lift $ setExpireCIFlag user False
expireChat user globalTTL `catchChatError` (toView . CEvtChatError (Just user)) expireChat user globalTTL `catchChatError` eToView
lift $ setChatItemsExpiration user globalTTL ttlCount lift $ setChatItemsExpiration user globalTTL ttlCount
ok user ok user
where where
@ -1478,15 +1474,15 @@ processChatCommand' vr = \case
liftIO $ updateGroupSettings db user chatId chatSettings liftIO $ updateGroupSettings db user chatId chatSettings
pure ms pure ms
forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId -> forM_ (filter memberActive ms) $ \m -> forM_ (memberConnId m) $ \connId ->
withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` (toView . CEvtChatError (Just user)) withAgent (\a -> toggleConnectionNtfs a connId $ chatHasNtfs chatSettings) `catchChatError` eToView
ok user ok user
_ -> pure $ chatCmdError (Just user) "not supported" _ -> throwCmdError "not supported"
APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do
m <- withFastStore $ \db -> do m <- withFastStore $ \db -> do
liftIO $ updateGroupMemberSettings db user gId gMemberId settings liftIO $ updateGroupMemberSettings db user gId gMemberId settings
getGroupMember db vr user gId gMemberId getGroupMember db vr user gId gMemberId
let ntfOn = showMessages $ memberSettings m let ntfOn = showMessages $ memberSettings m
toggleNtf user m ntfOn toggleNtf m ntfOn
ok user ok user
APIContactInfo contactId -> withUser $ \user@User {userId} -> do APIContactInfo contactId -> withUser $ \user@User {userId} -> do
-- [incognito] print user's incognito profile for this contact -- [incognito] print user's incognito profile for this contact
@ -1704,7 +1700,7 @@ processChatCommand' vr = \case
forM_ customUserProfileId $ \profileId -> forM_ customUserProfileId $ \profileId ->
deletePCCIncognitoProfile db user profileId deletePCCIncognitoProfile db user profileId
createDirectConnection db newUser agConnId ccLink' ConnNew Nothing subMode initialChatVersion PQSupportOn createDirectConnection db newUser agConnId ccLink' ConnNew Nothing subMode initialChatVersion PQSupportOn
deleteAgentConnectionAsync user (aConnId' conn) deleteAgentConnectionAsync (aConnId' conn)
pure conn' pure conn'
APIConnectPlan userId cLink -> withUserId userId $ \user -> APIConnectPlan userId cLink -> withUserId userId $ \user ->
uncurry (CRConnectionPlan user) <$> connectPlan user cLink uncurry (CRConnectionPlan user) <$> connectPlan user cLink
@ -1779,7 +1775,7 @@ processChatCommand' vr = \case
APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do
conns <- withFastStore $ \db -> getUserAddressConnections db vr user conns <- withFastStore $ \db -> getUserAddressConnections db vr user
withChatLock "deleteMyAddress" $ do withChatLock "deleteMyAddress" $ do
deleteAgentConnectionsAsync user $ map aConnId conns deleteAgentConnectionsAsync $ map aConnId conns
withFastStore' (`deleteUserAddress` user) withFastStore' (`deleteUserAddress` user)
let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing}
r <- updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing r <- updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing
@ -2019,7 +2015,7 @@ processChatCommand' vr = \case
updateGroupMemberStatus db userId fromMember GSMemInvited updateGroupMemberStatus db userId fromMember GSMemInvited
updateGroupMemberStatus db userId membership GSMemInvited updateGroupMemberStatus db userId membership GSMemInvited
throwError e throwError e
updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CEvtChatError (Just user)) updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` eToView
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
Nothing -> throwChatError $ CEContactNotActive ct Nothing -> throwChatError $ CEContactNotActive ct
APIAcceptMember groupId gmId role -> withUser $ \user -> do APIAcceptMember groupId gmId role -> withUser $ \user -> do
@ -2048,7 +2044,7 @@ processChatCommand' vr = \case
(errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems (errs2, changed2, acis) <- changeRoleCurrentMems user g currentMems
unless (null acis) $ toView $ CEvtNewChatItems user acis unless (null acis) $ toView $ CEvtNewChatItems user acis
let errs = errs1 <> errs2 let errs = errs1 <> errs2
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole} -- same order is not guaranteed pure $ CRMembersRoleUser {user, groupInfo = gInfo, members = changed1 <> changed2, toRole = newRole} -- same order is not guaranteed
where where
selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds
@ -2136,9 +2132,9 @@ processChatCommand' vr = \case
let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_ let acis = map (AChatItem SCTGroup SMDSnd (GroupChat gInfo)) $ rights cis_
unless (null acis) $ toView $ CEvtNewChatItems user acis unless (null acis) $ toView $ CEvtNewChatItems user acis
(errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems) (errs, blocked) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (updateGroupMemberBlocked db user gInfo mrs) blockMems)
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
-- TODO not batched - requires agent batch api -- TODO not batched - requires agent batch api
forM_ blocked $ \m -> toggleNtf user m (not blockFlag) forM_ blocked $ \m -> toggleNtf m (not blockFlag)
pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag} pure CRMembersBlockedForAllUser {user, groupInfo = gInfo, members = blocked, blocked = blockFlag}
where where
sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c sndItemData :: GroupMember -> SndMessage -> NewSndChatItemData c
@ -2161,7 +2157,7 @@ processChatCommand' vr = \case
acis = acis2 <> acis3 acis = acis2 <> acis3
errs = errs1 <> errs2 <> errs3 errs = errs1 <> errs2 <> errs3
unless (null acis) $ toView $ CEvtNewChatItems user acis unless (null acis) $ toView $ CEvtNewChatItems user acis
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
when withMessages $ deleteMessages user gInfo $ currentMems <> pendingMems when withMessages $ deleteMessages user gInfo $ currentMems <> pendingMems
pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2 <> deleted3) withMessages -- same order is not guaranteed pure $ CRUserDeletedMembers user gInfo (deleted1 <> deleted2 <> deleted3) withMessages -- same order is not guaranteed
where where
@ -2357,7 +2353,7 @@ processChatCommand' vr = \case
LastChats count_ -> withUser' $ \user -> do LastChats count_ -> withUser' $ \user -> do
let count = fromMaybe 5000 count_ let count = fromMaybe 5000 count_
(errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters)
unless (null errs) $ toView $ CEvtChatErrors (Just user) (map ChatErrorStore errs) unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs)
pure $ CRChats previews pure $ CRChats previews
LastMessages (Just chatName) count search -> withUser $ \user -> do LastMessages (Just chatName) count search -> withUser $ \user -> do
chatRef <- getChatRef user chatName chatRef <- getChatRef user chatName
@ -2403,7 +2399,7 @@ processChatCommand' vr = \case
processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)]
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" SendFileDescription _chatName _f -> throwCmdError "TODO"
-- TODO to use priority transactions we need a parameter that differentiates manual and automatic acceptance -- TODO to use priority transactions we need a parameter that differentiates manual and automatic acceptance
ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
withFileLock "receiveFile" fileId . procCmd $ do withFileLock "receiveFile" fileId . procCmd $ do
@ -2426,7 +2422,7 @@ processChatCommand' vr = \case
throwChatError $ CEFileCancel fileId "file transfer is complete" throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> do | otherwise -> do
fileAgentConnIds <- cancelSndFile user ftm fts True fileAgentConnIds <- cancelSndFile user ftm fts True
deleteAgentConnectionsAsync user fileAgentConnIds deleteAgentConnectionsAsync fileAgentConnIds
withFastStore (\db -> liftIO $ lookupChatRefByFileId db user fileId) >>= \case withFastStore (\db -> liftIO $ lookupChatRefByFileId db user fileId) >>= \case
Nothing -> pure () Nothing -> pure ()
Just (ChatRef CTDirect contactId) -> do Just (ChatRef CTDirect contactId) -> do
@ -2447,7 +2443,7 @@ processChatCommand' vr = \case
| rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete" | rcvFileComplete fileStatus -> throwChatError $ CEFileCancel fileId "file transfer is complete"
| otherwise -> case xftpRcvFile of | otherwise -> case xftpRcvFile of
Nothing -> do Nothing -> do
cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ftr >>= mapM_ deleteAgentConnectionAsync
ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId
pure $ CRRcvFileCancelled user ci ftr pure $ CRRcvFileCancelled user ci ftr
Just XFTPRcvFile {agentRcvFileId} -> do Just XFTPRcvFile {agentRcvFileId} -> do
@ -2595,8 +2591,9 @@ processChatCommand' vr = \case
GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions
GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo
-- CustomChatCommand is unsupported, it can be processed in preCmdHook -- CustomChatCommand is unsupported, it can be processed in preCmdHook
-- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand -- in a modified CLI app or core - the hook should return Either (Either ChatError ChatResponse) ChatCommand,
CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported" -- where Left means command result, and Right some other command to be processed by this function.
CustomChatCommand _cmd -> withUser $ \_ -> throwCmdError "not supported"
where where
procCmd :: CM ChatResponse -> CM ChatResponse procCmd :: CM ChatResponse -> CM ChatResponse
procCmd = id procCmd = id
@ -2762,7 +2759,7 @@ processChatCommand' vr = \case
let idsEvts = L.map ctSndEvent changedCts let idsEvts = L.map ctSndEvent changedCts
msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts msgReqs_ <- lift $ L.zipWith ctMsgReq changedCts <$> createSndMessages idsEvts
(errs, cts) <- partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_ (errs, cts) <- partitionEithers . L.toList . L.zipWith (second . const) changedCts <$> deliverMessagesB msgReqs_
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts let changedCts' = filter (\ChangedProfileContact {ct, ct'} -> directOrUsed ct' && mergedPreferences ct' /= mergedPreferences ct) cts
lift $ createContactsSndFeatureItems user' changedCts' lift $ createContactsSndFeatureItems user' changedCts'
pure pure
@ -2802,7 +2799,7 @@ processChatCommand' vr = \case
mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False
when (mergedProfile' /= mergedProfile) $ when (mergedProfile' /= mergedProfile) $
withContactLock "updateProfile" (contactId' ct) $ do withContactLock "updateProfile" (contactId' ct) $ do
void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` (toView . CEvtChatError (Just user)) void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` eToView
lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct'
pure $ CRContactPrefsUpdated user ct ct' pure $ CRContactPrefsUpdated user ct ct'
runUpdateGroupProfile :: User -> Group -> GroupProfile -> CM ChatResponse runUpdateGroupProfile :: User -> Group -> GroupProfile -> CM ChatResponse
@ -3005,7 +3002,7 @@ processChatCommand' vr = \case
deleteCIFiles user filesInfo deleteCIFiles user filesInfo
withAgent (\a -> deleteUser a (aUserId user) delSMPQueues) withAgent (\a -> deleteUser a (aUserId user) delSMPQueues)
`catchChatError` \case `catchChatError` \case
e@(ChatErrorAgent NO_USER _) -> toView $ CEvtChatError (Just user) e e@(ChatErrorAgent NO_USER _) -> eToView e
e -> throwError e e -> throwError e
withFastStore' (`deleteUserRecord` user) withFastStore' (`deleteUserRecord` user)
when (activeUser user) $ chatWriteVar currentUser Nothing when (activeUser user) $ chatWriteVar currentUser Nothing
@ -3058,7 +3055,7 @@ processChatCommand' vr = \case
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
connectWithPlan user@User {userId} incognito ccLink plan connectWithPlan user@User {userId} incognito ccLink plan
| connectionPlanProceed plan = do | connectionPlanProceed plan = do
case plan of CPError e -> toView $ CEvtChatError (Just user) e; _ -> pure () case plan of CPError e -> eToView e; _ -> pure ()
case plan of case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) -> CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand $ APIConnectContactViaAddress userId incognito contactId processChatCommand $ APIConnectContactViaAddress userId incognito contactId
@ -3208,7 +3205,7 @@ processChatCommand' vr = \case
let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) msgs_ let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) msgs_
when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch" when (length itemsData /= length cmrs) $ logError "sendContactContentMessages: cmrs and itemsData length mismatch"
r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) Nothing itemsData timed_ live r@(_, cis) <- partitionEithers <$> saveSndChatItems user (CDDirectSnd ct) Nothing itemsData timed_ live
processSendErrs user r processSendErrs r
forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ (timed_ >>= timedDeleteAt') $ \deleteAt ->
forM_ cis $ \ci -> forM_ cis $ \ci ->
startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) deleteAt startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) deleteAt
@ -3288,7 +3285,7 @@ processChatCommand' vr = \case
when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch" when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch"
createMemberSndStatuses cis_ msgs_ gsr createMemberSndStatuses cis_ msgs_ gsr
let r@(_, cis) = partitionEithers cis_ let r@(_, cis) = partitionEithers cis_
processSendErrs user r processSendErrs r
forM_ (timed_ >>= timedDeleteAt') $ \deleteAt -> forM_ (timed_ >>= timedDeleteAt') $ \deleteAt ->
forM_ cis $ \ci -> forM_ cis $ \ci ->
startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) deleteAt startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) deleteAt
@ -3358,7 +3355,7 @@ processChatCommand' vr = \case
case contactOrGroup of case contactOrGroup of
CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> CGContact Contact {activeConn} -> forM_ activeConn $ \conn ->
withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr
CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CEvtChatError (Just user)) CGGroup _ ms -> forM_ ms $ \m -> saveMemberFD m `catchChatError` eToView
where where
-- we are not sending files to pending members, same as with inline files -- we are not sending files to pending members, same as with inline files
saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} =
@ -3377,23 +3374,23 @@ processChatCommand' vr = \case
zipWith4 $ \(ComposedMessage {msgContent}, itemForwarded, ts, mm) f q -> \case zipWith4 $ \(ComposedMessage {msgContent}, itemForwarded, ts, mm) f q -> \case
Right msg -> Right $ NewSndChatItemData msg (CISndMsgContent msgContent) ts mm f q itemForwarded Right msg -> Right $ NewSndChatItemData msg (CISndMsgContent msgContent) ts mm f q itemForwarded
Left e -> Left e -- step over original error Left e -> Left e -- step over original error
processSendErrs :: User -> ([ChatError], [ChatItem c d]) -> CM () processSendErrs :: ([ChatError], [ChatItem c d]) -> CM ()
processSendErrs user = \case processSendErrs = \case
-- no errors -- no errors
([], _) -> pure () ([], _) -> pure ()
-- at least one item is successfully created -- at least one item is successfully created
(errs, _ci : _) -> toView $ CEvtChatErrors (Just user) errs (errs, _ci : _) -> toView $ CEvtChatErrors errs
-- single error -- single error
([err], []) -> throwError err ([err], []) -> throwError err
-- multiple errors -- multiple errors
(errs@(err : _), []) -> do (errs@(err : _), []) -> do
toView $ CEvtChatErrors (Just user) errs toView $ CEvtChatErrors errs
throwError err throwError err
getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect]) getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect])
getCommandDirectChatItems user ctId itemIds = do getCommandDirectChatItems user ctId itemIds = do
ct <- withFastStore $ \db -> getContact db vr user ctId ct <- withFastStore $ \db -> getContact db vr user ctId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds))
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure (ct, items) pure (ct, items)
where where
getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect)) getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect))
@ -3402,7 +3399,7 @@ processChatCommand' vr = \case
getCommandGroupChatItems user gId itemIds = do getCommandGroupChatItems user gId itemIds = do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db gInfo) (L.toList itemIds)) (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db gInfo) (L.toList itemIds))
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure (gInfo, items) pure (gInfo, items)
where where
getGroupCI :: DB.Connection -> GroupInfo -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) getGroupCI :: DB.Connection -> GroupInfo -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup))
@ -3411,7 +3408,7 @@ processChatCommand' vr = \case
getCommandLocalChatItems user nfId itemIds = do getCommandLocalChatItems user nfId itemIds = do
nf <- withStore $ \db -> getNoteFolder db user nfId nf <- withStore $ \db -> getNoteFolder db user nfId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds)) (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds))
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure (nf, items) pure (nf, items)
where where
getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal)) getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal))
@ -3536,7 +3533,7 @@ startExpireCIThread user@User {userId} = do
liftIO $ threadDelay' delay liftIO $ threadDelay' delay
interval <- asks $ ciExpirationInterval . config interval <- asks $ ciExpirationInterval . config
forever $ do forever $ do
flip catchChatError' (toView' . CEvtChatError (Just user)) $ do flip catchChatError' (eToView') $ do
expireFlags <- asks expireCIFlags expireFlags <- asks expireCIFlags
atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry atomically $ TM.lookup userId expireFlags >>= \b -> unless (b == Just True) retry
lift waitChatStartedAndActivated lift waitChatStartedAndActivated
@ -3568,7 +3565,7 @@ agentSubscriber = do
q <- asks $ subQ . smpAgent q <- asks $ subQ . smpAgent
forever (atomically (readTBQueue q) >>= process) forever (atomically (readTBQueue q) >>= process)
`E.catchAny` \e -> do `E.catchAny` \e -> do
toView' $ CEvtChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing eToView' $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing
E.throwIO e E.throwIO e
where where
process :: (ACorrId, AEntityId, AEvt) -> CM' () process :: (ACorrId, AEntityId, AEvt) -> CM' ()
@ -3578,7 +3575,7 @@ agentSubscriber = do
SAERcvFile -> processAgentMsgRcvFile corrId entId msg SAERcvFile -> processAgentMsgRcvFile corrId entId msg
SAESndFile -> processAgentMsgSndFile corrId entId msg SAESndFile -> processAgentMsgSndFile corrId entId msg
where where
run action = action `catchChatError'` (toView' . CEvtChatError Nothing) run action = action `catchChatError'` (eToView')
type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ())) type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ()))
@ -3739,7 +3736,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM () pendingConnSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId PendingContactConnection -> CM ()
pendingConnSubsToView rs = toViewTE . TEPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs pendingConnSubsToView rs = toViewTE . TEPendingSubSummary user . map (uncurry PendingSubStatus) . resultsFor rs
withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a] withStore_ :: (DB.Connection -> User -> IO [a]) -> CM [a]
withStore_ a = withStore' (`a` user) `catchChatError` \e -> toView (CEvtChatError (Just user) e) $> [] withStore_ a = withStore' (`a` user) `catchChatError` \e -> eToView e $> []
filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)] filterErrors :: [(a, Maybe ChatError)] -> [(a, ChatError)]
filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_) filterErrors = mapMaybe (\(a, e_) -> (a,) <$> e_)
resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)] resultsFor :: Map ConnId (Either AgentErrorType ()) -> Map ConnId a -> [(a, Maybe ChatError)]
@ -3761,28 +3758,28 @@ cleanupManager = do
liftIO $ threadDelay' initialDelay liftIO $ threadDelay' initialDelay
stepDelay <- asks (cleanupManagerStepDelay . config) stepDelay <- asks (cleanupManagerStepDelay . config)
forever $ do forever $ do
flip catchChatError (toView . CEvtChatError Nothing) $ do flip catchChatError eToView $ do
lift waitChatStartedAndActivated lift waitChatStartedAndActivated
users <- withStore' getUsers users <- withStore' getUsers
let (us, us') = partition activeUser users let (us, us') = partition activeUser users
forM_ us $ cleanupUser interval stepDelay forM_ us $ cleanupUser interval stepDelay
forM_ us' $ cleanupUser interval stepDelay forM_ us' $ cleanupUser interval stepDelay
cleanupMessages `catchChatError` (toView . CEvtChatError Nothing) cleanupMessages `catchChatError` eToView
-- TODO possibly, also cleanup async commands -- TODO possibly, also cleanup async commands
cleanupProbes `catchChatError` (toView . CEvtChatError Nothing) cleanupProbes `catchChatError` eToView
liftIO $ threadDelay' $ diffToMicroseconds interval liftIO $ threadDelay' $ diffToMicroseconds interval
where where
runWithoutInitialDelay cleanupInterval = flip catchChatError (toView . CEvtChatError Nothing) $ do runWithoutInitialDelay cleanupInterval = flip catchChatError eToView $ do
lift waitChatStartedAndActivated lift waitChatStartedAndActivated
users <- withStore' getUsers users <- withStore' getUsers
let (us, us') = partition activeUser users let (us, us') = partition activeUser users
forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CEvtChatError (Just u)) forM_ us $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView
forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` (toView . CEvtChatError (Just u)) forM_ us' $ \u -> cleanupTimedItems cleanupInterval u `catchChatError` eToView
cleanupUser cleanupInterval stepDelay user = do cleanupUser cleanupInterval stepDelay user = do
cleanupTimedItems cleanupInterval user `catchChatError` (toView . CEvtChatError (Just user)) cleanupTimedItems cleanupInterval user `catchChatError` eToView
liftIO $ threadDelay' stepDelay liftIO $ threadDelay' stepDelay
-- TODO remove in future versions: legacy step - contacts are no longer marked as deleted -- TODO remove in future versions: legacy step - contacts are no longer marked as deleted
cleanupDeletedContacts user `catchChatError` (toView . CEvtChatError (Just user)) cleanupDeletedContacts user `catchChatError` eToView
liftIO $ threadDelay' stepDelay liftIO $ threadDelay' stepDelay
cleanupTimedItems cleanupInterval user = do cleanupTimedItems cleanupInterval user = do
ts <- liftIO getCurrentTime ts <- liftIO getCurrentTime
@ -3794,7 +3791,7 @@ cleanupManager = do
contacts <- withStore' $ \db -> getDeletedContacts db vr user contacts <- withStore' $ \db -> getDeletedContacts db vr user
forM_ contacts $ \ct -> forM_ contacts $ \ct ->
withStore (\db -> deleteContactWithoutGroups db user ct) withStore (\db -> deleteContactWithoutGroups db user ct)
`catchChatError` (toView . CEvtChatError (Just user)) `catchChatError` eToView
cleanupMessages = do cleanupMessages = do
ts <- liftIO getCurrentTime ts <- liftIO getCurrentTime
let cutoffTs = addUTCTime (-(30 * nominalDay)) ts let cutoffTs = addUTCTime (-(30 * nominalDay)) ts
@ -3820,7 +3817,7 @@ expireChatItems user@User {userId} globalTTL sync = do
loop :: [Int64] -> (Int64 -> CM ()) -> CM () loop :: [Int64] -> (Int64 -> CM ()) -> CM ()
loop [] _ = pure () loop [] _ = pure ()
loop (a : as) process = continue $ do loop (a : as) process = continue $ do
process a `catchChatError` (toView . CEvtChatError (Just user)) process a `catchChatError` eToView
loop as process loop as process
continue :: CM () -> CM () continue :: CM () -> CM ()
continue a = continue a =

View file

@ -184,11 +184,11 @@ callTimed ct aciContent =
aciContentCallStatus (ACIContent _ (CIRcvCall st _)) = Just st aciContentCallStatus (ACIContent _ (CIRcvCall st _)) = Just st
aciContentCallStatus _ = Nothing aciContentCallStatus _ = Nothing
toggleNtf :: User -> GroupMember -> Bool -> CM () toggleNtf :: GroupMember -> Bool -> CM ()
toggleNtf user m ntfOn = toggleNtf m ntfOn =
when (memberActive m) $ when (memberActive m) $
forM_ (memberConnId m) $ \connId -> forM_ (memberConnId m) $ \connId ->
withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` (toView . CEvtChatError (Just user)) withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchChatError` eToView
prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup)) prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))
prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of prepareGroupMsg db user g@GroupInfo {membership} mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = case (quotedItemId_, itemForwarded) of
@ -388,8 +388,8 @@ cancelFilesInProgress user filesInfo = do
lift $ agentXFTPDeleteRcvFiles xrfIds lift $ agentXFTPDeleteRcvFiles xrfIds
let smpSFConnIds = concatMap (\(ft, sfts) -> mapMaybe (smpSndFileConnId ft) sfts) sfs let smpSFConnIds = concatMap (\(ft, sfts) -> mapMaybe (smpSndFileConnId ft) sfts) sfs
smpRFConnIds = mapMaybe smpRcvFileConnId rfs smpRFConnIds = mapMaybe smpRcvFileConnId rfs
deleteAgentConnectionsAsync user smpSFConnIds deleteAgentConnectionsAsync smpSFConnIds
deleteAgentConnectionsAsync user smpRFConnIds deleteAgentConnectionsAsync smpRFConnIds
where where
fileEnded CIFileInfo {fileStatus} = case fileStatus of fileEnded CIFileInfo {fileStatus} = case fileStatus of
Just (AFS _ status) -> ciFileEnded status Just (AFS _ status) -> ciFileEnded status
@ -446,7 +446,7 @@ deleteDirectCIs user ct items = do
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
deleteCIFiles user ciFilesInfo deleteCIFiles user ciFilesInfo
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items)
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure deletions pure deletions
where where
deleteItem db (CChatItem md ci) = do deleteItem db (CChatItem md ci) = do
@ -458,7 +458,7 @@ deleteGroupCIs user gInfo items byGroupMember_ deletedTs = do
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
deleteCIFiles user ciFilesInfo deleteCIFiles user ciFilesInfo
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items)
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure deletions pure deletions
where where
deleteItem :: DB.Connection -> CChatItem 'CTGroup -> IO ChatItemDeletion deleteItem :: DB.Connection -> CChatItem 'CTGroup -> IO ChatItemDeletion
@ -491,7 +491,7 @@ deleteLocalCIs user nf items byUser timed = do
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
deleteFilesLocally ciFilesInfo deleteFilesLocally ciFilesInfo
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items)
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure $ CRChatItemsDeleted user deletions byUser timed pure $ CRChatItemsDeleted user deletions byUser timed
where where
deleteItem db (CChatItem md ci) = do deleteItem db (CChatItem md ci) = do
@ -510,7 +510,7 @@ markDirectCIsDeleted user ct items deletedTs = do
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
cancelFilesInProgress user ciFilesInfo cancelFilesInProgress user ciFilesInfo
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items)
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure deletions pure deletions
where where
markDeleted db (CChatItem md ci) = do markDeleted db (CChatItem md ci) = do
@ -522,7 +522,7 @@ markGroupCIsDeleted user gInfo items byGroupMember_ deletedTs = do
let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items
cancelFilesInProgress user ciFilesInfo cancelFilesInProgress user ciFilesInfo
(errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items)
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure deletions pure deletions
-- pure $ CRChatItemsDeleted user deletions byUser False -- pure $ CRChatItemsDeleted user deletions byUser False
where where
@ -998,7 +998,7 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac
forM_ (L.nonEmpty events) $ \events' -> forM_ (L.nonEmpty events) $ \events' ->
sendGroupMemberMessages user conn events' groupId sendGroupMemberMessages user conn events' groupId
else forM_ shuffledIntros $ \intro -> else forM_ shuffledIntros $ \intro ->
processIntro intro `catchChatError` (toView . CEvtChatError (Just user)) processIntro intro `catchChatError` eToView
memberIntro :: GroupMember -> ChatMsgEvent 'Json memberIntro :: GroupMember -> ChatMsgEvent 'Json
memberIntro reMember = memberIntro reMember =
let mInfo = memberInfo reMember let mInfo = memberInfo reMember
@ -1021,7 +1021,7 @@ introduceToGroup vr user gInfo@GroupInfo {groupId, membership} m@GroupMember {ac
(errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100)
(errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items (errs', events) <- partitionEithers <$> mapM (tryChatError . itemForwardEvents) items
let errors = map ChatErrorStore errs <> errs' let errors = map ChatErrorStore errs <> errs'
unless (null errors) $ toView $ CEvtChatErrors (Just user) errors unless (null errors) $ toView $ CEvtChatErrors errors
let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_ let events' = maybe (concat events) (\x -> concat events <> [x]) descrEvent_
forM_ (L.nonEmpty events') $ \events'' -> forM_ (L.nonEmpty events') $ \events'' ->
sendGroupMemberMessages user conn events'' groupId sendGroupMemberMessages user conn events'' groupId
@ -1121,7 +1121,7 @@ deleteGroupLinkIfExists user gInfo = do
deleteGroupLink_ :: User -> GroupInfo -> Connection -> CM () deleteGroupLink_ :: User -> GroupInfo -> Connection -> CM ()
deleteGroupLink_ user gInfo conn = do deleteGroupLink_ user gInfo conn = do
deleteAgentConnectionAsync user $ aConnId conn deleteAgentConnectionAsync $ aConnId conn
withStore' $ \db -> deleteGroupLink db user gInfo withStore' $ \db -> deleteGroupLink db user gInfo
startProximateTimedItemThread :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM () startProximateTimedItemThread :: User -> (ChatRef, ChatItemId) -> UTCTime -> CM ()
@ -1162,7 +1162,7 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do
deletedTs <- liftIO getCurrentTime deletedTs <- liftIO getCurrentTime
deletions <- deleteGroupCIs user gInfo [ci] Nothing deletedTs deletions <- deleteGroupCIs user gInfo [ci] Nothing deletedTs
toView $ CEvtChatItemsDeleted user deletions True True toView $ CEvtChatItemsDeleted user deletions True True
_ -> toView . CEvtChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType" _ -> eToView $ ChatError $ CEInternalError "bad deleteTimedItem cType"
startUpdatedTimedItemThread :: User -> ChatRef -> ChatItem c d -> ChatItem c d -> CM () startUpdatedTimedItemThread :: User -> ChatRef -> ChatItem c d -> ChatItem c d -> CM ()
startUpdatedTimedItemThread user chatRef ci ci' = startUpdatedTimedItemThread user chatRef ci ci' =
@ -1289,7 +1289,7 @@ sendFileChunk user ft@SndFileTransfer {fileId, fileStatus, agentConnId = AgentCo
updateDirectCIFileStatus db vr user fileId CIFSSndComplete updateDirectCIFileStatus db vr user fileId CIFSSndComplete
toView $ CEvtSndFileComplete user ci ft toView $ CEvtSndFileComplete user ci ft
lift $ closeFileHandle fileId sndFiles lift $ closeFileHandle fileId sndFiles
deleteAgentConnectionAsync user acId deleteAgentConnectionAsync acId
sendFileChunkNo :: SndFileTransfer -> Integer -> CM () sendFileChunkNo :: SndFileTransfer -> Integer -> CM ()
sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do sendFileChunkNo ft@SndFileTransfer {agentConnId = AgentConnId acId} chunkNo = do
@ -1337,7 +1337,7 @@ appendFileChunk ft@RcvFileTransfer {fileId, fileStatus, cryptoArgs, fileInvitati
removeFile fsFilePath `catchChatError` \_ -> pure () removeFile fsFilePath `catchChatError` \_ -> pure ()
renameFile tmpFile fsFilePath renameFile tmpFile fsFilePath
Left e -> do Left e -> do
toView $ CEvtChatError Nothing e eToView e
removeFile tmpFile `catchChatError` \_ -> pure () removeFile tmpFile `catchChatError` \_ -> pure ()
withStore' (`removeFileCryptoArgs` fileId) withStore' (`removeFileCryptoArgs` fileId)
where where
@ -1362,7 +1362,7 @@ isFileActive fileId files = do
cancelRcvFileTransfer :: User -> RcvFileTransfer -> CM (Maybe ConnId) cancelRcvFileTransfer :: User -> RcvFileTransfer -> CM (Maybe ConnId)
cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} = cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInline} =
cancel' `catchChatError` (\e -> toView (CEvtChatError (Just user) e) $> fileConnId) cancel' `catchChatError` (\e -> eToView e $> fileConnId)
where where
cancel' = do cancel' = do
lift $ closeFileHandle fileId rcvFiles lift $ closeFileHandle fileId rcvFiles
@ -1380,13 +1380,13 @@ cancelRcvFileTransfer user ft@RcvFileTransfer {fileId, xftpRcvFile, rcvFileInlin
cancelSndFile :: User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> CM [ConnId] cancelSndFile :: User -> FileTransferMeta -> [SndFileTransfer] -> Bool -> CM [ConnId]
cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do cancelSndFile user FileTransferMeta {fileId, xftpSndFile} fts sendCancel = do
withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled) withStore' (\db -> updateFileCancelled db user fileId CIFSSndCancelled)
`catchChatError` (toView . CEvtChatError (Just user)) `catchChatError` eToView
case xftpSndFile of case xftpSndFile of
Nothing -> Nothing ->
catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel) catMaybes <$> forM fts (\ft -> cancelSndFileTransfer user ft sendCancel)
Just xsf -> do Just xsf -> do
forM_ fts (\ft -> cancelSndFileTransfer user ft False) forM_ fts (\ft -> cancelSndFileTransfer user ft False)
lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchChatError` (toView . CEvtChatError (Just user)) lift (agentXFTPDeleteSndFileRemote user xsf fileId) `catchChatError` eToView
pure [] pure []
-- TODO v6.0 remove -- TODO v6.0 remove
@ -1394,7 +1394,7 @@ cancelSndFileTransfer :: User -> SndFileTransfer -> Bool -> CM (Maybe ConnId)
cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel = cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, agentConnId = AgentConnId acId, fileStatus, fileInline} sendCancel =
if fileStatus == FSCancelled || fileStatus == FSComplete if fileStatus == FSCancelled || fileStatus == FSComplete
then pure Nothing then pure Nothing
else cancel' `catchChatError` (\e -> toView (CEvtChatError (Just user) e) $> fileConnId) else cancel' `catchChatError` (\e -> eToView e $> fileConnId)
where where
cancel' = do cancel' = do
withStore' $ \db -> do withStore' $ \db -> do
@ -1421,16 +1421,16 @@ deleteMembersConnections user members = deleteMembersConnections' user members F
deleteMembersConnections' :: User -> [GroupMember] -> Bool -> CM () deleteMembersConnections' :: User -> [GroupMember] -> Bool -> CM ()
deleteMembersConnections' user members waitDelivery = do deleteMembersConnections' user members waitDelivery = do
let memberConns = mapMaybe (\GroupMember {activeConn} -> activeConn) members let memberConns = mapMaybe (\GroupMember {activeConn} -> activeConn) members
deleteAgentConnectionsAsync' user (map aConnId memberConns) waitDelivery deleteAgentConnectionsAsync' (map aConnId memberConns) waitDelivery
lift . void . withStoreBatch' $ \db -> map (\Connection {connId} -> deleteConnectionRecord db user connId) memberConns lift . void . withStoreBatch' $ \db -> map (\Connection {connId} -> deleteConnectionRecord db user connId) memberConns
deleteMemberConnection :: User -> GroupMember -> CM () deleteMemberConnection :: GroupMember -> CM ()
deleteMemberConnection user mem = deleteMemberConnection' user mem False deleteMemberConnection mem = deleteMemberConnection' mem False
deleteMemberConnection' :: User -> GroupMember -> Bool -> CM () deleteMemberConnection' :: GroupMember -> Bool -> CM ()
deleteMemberConnection' user GroupMember {activeConn} waitDelivery = do deleteMemberConnection' GroupMember {activeConn} waitDelivery = do
forM_ activeConn $ \conn -> do forM_ activeConn $ \conn -> do
deleteAgentConnectionAsync' user (aConnId conn) waitDelivery deleteAgentConnectionAsync' (aConnId conn) waitDelivery
withStore' $ \db -> updateConnectionStatus db conn ConnDeleted withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
deleteOrUpdateMemberRecord :: User -> GroupMember -> CM () deleteOrUpdateMemberRecord :: User -> GroupMember -> CM ()
@ -1515,7 +1515,7 @@ sendGroupMemberMessages user conn events groupId = do
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn) when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
let idsEvts = L.map (GroupId groupId,) events let idsEvts = L.map (GroupId groupId,) events
(errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
forM_ (L.nonEmpty msgs) $ \msgs' -> forM_ (L.nonEmpty msgs) $ \msgs' ->
batchSendConnMessages user conn MsgFlags {notification = True} msgs' batchSendConnMessages user conn MsgFlags {notification = True} msgs'
@ -1644,7 +1644,7 @@ sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> Non
sendGroupMessages user gInfo members events = do sendGroupMessages user gInfo members events = do
-- TODO [knocking] when sending to all, send profile update to pending approval members too, then filter for next step? -- TODO [knocking] when sending to all, send profile update to pending approval members too, then filter for next step?
when shouldSendProfileUpdate $ when shouldSendProfileUpdate $
sendProfileUpdate `catchChatError` (toView . CEvtChatError (Just user)) sendProfileUpdate `catchChatError` eToView
sendGroupMessages_ user gInfo members events sendGroupMessages_ user gInfo members events
where where
User {profile = p, userMemberProfileUpdatedAt} = user User {profile = p, userMemberProfileUpdatedAt} = user
@ -1800,10 +1800,10 @@ memberSendAction gInfo events members m@GroupMember {memberRole, memberStatus} =
XGrpMsgForward {} -> True XGrpMsgForward {} -> True
_ -> False _ -> False
sendGroupMemberMessage :: MsgEncodingI e => User -> GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM () sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM ()
sendGroupMemberMessage user gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do
msg <- createSndMessage chatMsgEvent (GroupId groupId) msg <- createSndMessage chatMsgEvent (GroupId groupId)
messageMember msg `catchChatError` (toView . CEvtChatError (Just user)) messageMember msg `catchChatError` eToView
where where
messageMember :: SndMessage -> CM () messageMember :: SndMessage -> CM ()
messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case
@ -1986,20 +1986,22 @@ agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do
connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode
pure (cmdId, connId) pure (cmdId, connId)
deleteAgentConnectionAsync :: User -> ConnId -> CM () deleteAgentConnectionAsync :: ConnId -> CM ()
deleteAgentConnectionAsync user acId = deleteAgentConnectionAsync' user acId False deleteAgentConnectionAsync acId = deleteAgentConnectionAsync' acId False
{-# INLINE deleteAgentConnectionAsync #-}
deleteAgentConnectionAsync' :: User -> ConnId -> Bool -> CM () deleteAgentConnectionAsync' :: ConnId -> Bool -> CM ()
deleteAgentConnectionAsync' user acId waitDelivery = do deleteAgentConnectionAsync' acId waitDelivery = do
withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` (toView . CEvtChatError (Just user)) withAgent (\a -> deleteConnectionAsync a waitDelivery acId) `catchChatError` eToView
deleteAgentConnectionsAsync :: User -> [ConnId] -> CM () deleteAgentConnectionsAsync :: [ConnId] -> CM ()
deleteAgentConnectionsAsync user acIds = deleteAgentConnectionsAsync' user acIds False deleteAgentConnectionsAsync acIds = deleteAgentConnectionsAsync' acIds False
{-# INLINE deleteAgentConnectionsAsync #-}
deleteAgentConnectionsAsync' :: User -> [ConnId] -> Bool -> CM () deleteAgentConnectionsAsync' :: [ConnId] -> Bool -> CM ()
deleteAgentConnectionsAsync' _ [] _ = pure () deleteAgentConnectionsAsync' [] _ = pure ()
deleteAgentConnectionsAsync' user acIds waitDelivery = do deleteAgentConnectionsAsync' acIds waitDelivery = do
withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` (toView . CEvtChatError (Just user)) withAgent (\a -> deleteConnectionsAsync a waitDelivery acIds) `catchChatError` eToView
agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM () agentXFTPDeleteRcvFile :: RcvFileId -> FileTransferId -> CM ()
agentXFTPDeleteRcvFile aFileId fileId = do agentXFTPDeleteRcvFile aFileId fileId = do
@ -2100,7 +2102,7 @@ createContactsFeatureItems ::
createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do
let dirsCIContents = map contactChangedFeatures cts let dirsCIContents = map contactChangedFeatures cts
(errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents (errs, acis) <- partitionEithers <$> createInternalItemsForChats user Nothing dirsCIContents
unless (null errs) $ toView' $ CEvtChatErrors (Just user) errs unless (null errs) $ toView' $ CEvtChatErrors errs
toView' $ CEvtNewChatItems user acis toView' $ CEvtNewChatItems user acis
where where
contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d]) contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, [CIContent d])
@ -2182,7 +2184,7 @@ createLocalChatItems ::
createLocalChatItems user cd itemsData createdAt = do createLocalChatItems user cd itemsData createdAt = do
withStore' $ \db -> updateChatTs db user cd createdAt withStore' $ \db -> updateChatTs db user cd createdAt
(errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData) (errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData)
unless (null errs) $ toView $ CEvtChatErrors (Just user) errs unless (null errs) $ toView $ CEvtChatErrors errs
pure items pure items
where where
createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd) createItem :: DB.Connection -> (CIContent 'MDSnd, Maybe (CIFile 'MDSnd), Maybe CIForwardedFrom, (Text, Maybe MarkdownList)) -> IO (ChatItem 'CTLocal 'MDSnd)
@ -2195,9 +2197,7 @@ withUser' :: (User -> CM ChatResponse) -> CM ChatResponse
withUser' action = withUser' action =
asks currentUser asks currentUser
>>= readTVarIO >>= readTVarIO
>>= maybe (throwChatError CENoActiveUser) run >>= maybe (throwChatError CENoActiveUser) action
where
run u = action u `catchChatError` (pure . CRChatCmdError (Just u))
withUser :: (User -> CM ChatResponse) -> CM ChatResponse withUser :: (User -> CM ChatResponse) -> CM ChatResponse
withUser action = withUser' $ \user -> withUser action = withUser' $ \user ->

View file

@ -96,14 +96,14 @@ processAgentMessage _ _ (DEL_RCVQS delQs) =
processAgentMessage _ _ (DEL_CONNS connIds) = processAgentMessage _ _ (DEL_CONNS connIds) =
toView $ CEvtAgentConnsDeleted $ L.map AgentConnId connIds toView $ CEvtAgentConnsDeleted $ L.map AgentConnId connIds
processAgentMessage _ "" (ERR e) = processAgentMessage _ "" (ERR e) =
toView $ CEvtChatError Nothing $ ChatErrorAgent e Nothing eToView $ ChatErrorAgent e Nothing
processAgentMessage corrId connId msg = do processAgentMessage corrId connId msg = do
lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId)) lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId))
withEntityLock "processAgentMessage" lockEntity $ do withEntityLock "processAgentMessage" lockEntity $ do
vr <- chatVersionRange vr <- chatVersionRange
-- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here
critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case
Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CEvtChatError (Just user)) Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` eToView
_ -> throwChatError $ CENoConnectionUser (AgentConnId connId) _ -> throwChatError $ CENoConnectionUser (AgentConnId connId)
-- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps.
@ -144,7 +144,7 @@ processAgentMessageNoConn = \case
errsEvent cErrs = do errsEvent cErrs = do
vr <- chatVersionRange vr <- chatVersionRange
errs <- lift $ rights <$> withStoreBatch' (\db -> map (getChatErr vr db) cErrs) errs <- lift $ rights <$> withStoreBatch' (\db -> map (getChatErr vr db) cErrs)
toView $ CEvtChatErrors Nothing errs toView $ CEvtChatErrors errs
where where
getChatErr :: VersionRangeChat -> DB.Connection -> (ConnId, AgentErrorType) -> IO ChatError getChatErr :: VersionRangeChat -> DB.Connection -> (ConnId, AgentErrorType) -> IO ChatError
getChatErr vr db (connId, err) = getChatErr vr db (connId, err) =
@ -156,7 +156,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId)
withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $ withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $
withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CEvtChatError (Just user)) Just user -> process user fileId `catchChatError` eToView
_ -> do _ -> do
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId
@ -208,9 +208,9 @@ processAgentMsgSndFile _corrId aFileId msg = do
Just rs -> case L.last rs of Just rs -> case L.last rs of
Right ([msgDeliveryId], _) -> Right ([msgDeliveryId], _) ->
withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId
Right (deliveryIds, _) -> toView $ CEvtChatError (Just user) $ ChatError $ CEInternalError $ "SFDONE, sendFileDescriptions: expected 1 delivery id, got " <> show (length deliveryIds) Right (deliveryIds, _) -> eToView $ ChatError $ CEInternalError $ "SFDONE, sendFileDescriptions: expected 1 delivery id, got " <> show (length deliveryIds)
Left e -> toView $ CEvtChatError (Just user) e Left e -> eToView e
Nothing -> toView $ CEvtChatError (Just user) $ ChatError $ CEInternalError "SFDONE, sendFileDescriptions: expected at least 1 result" Nothing -> eToView $ ChatError $ CEInternalError "SFDONE, sendFileDescriptions: expected at least 1 result"
lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId)
(_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do
ms <- withStore' $ \db -> getGroupMembers db vr user g ms <- withStore' $ \db -> getGroupMembers db vr user g
@ -259,7 +259,7 @@ processAgentMsgSndFile _corrId aFileId msg = do
let (errs, msgReqs) = partitionEithers . L.toList $ L.zipWith (fmap . toMsgReq) connsIdsEvts sndMsgs_ let (errs, msgReqs) = partitionEithers . L.toList $ L.zipWith (fmap . toMsgReq) connsIdsEvts sndMsgs_
delivered <- mapM deliverMessages (L.nonEmpty msgReqs) delivered <- mapM deliverMessages (L.nonEmpty msgReqs)
let errs' = errs <> maybe [] (lefts . L.toList) delivered let errs' = errs <> maybe [] (lefts . L.toList) delivered
unless (null errs') $ toView $ CEvtChatErrors (Just user) errs' unless (null errs') $ toView $ CEvtChatErrors errs'
pure delivered pure delivered
where where
connDescrEvents :: Int -> NonEmpty (Connection, (ConnOrGroupId, ChatMsgEvent 'Json)) connDescrEvents :: Int -> NonEmpty (Connection, (ConnOrGroupId, ChatMsgEvent 'Json))
@ -298,7 +298,7 @@ processAgentMsgRcvFile _corrId aFileId msg = do
(cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId)
withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $ withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $
withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case
Just user -> process user fileId `catchChatError` (toView . CEvtChatError (Just user)) Just user -> process user fileId `catchChatError` eToView
_ -> do _ -> do
lift $ withAgent' (`xftpDeleteRcvFile` aFileId) lift $ withAgent' (`xftpDeleteRcvFile` aFileId)
throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId
@ -438,13 +438,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
MWARN _ err -> MWARN _ err ->
processConnMWARN connEntity conn err processConnMWARN connEntity conn err
MERR _ err -> do MERR _ err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
processConnMERR connEntity conn err processConnMERR connEntity conn err
MERRS _ err -> do MERRS _ err -> do
-- error cannot be AUTH error here -- error cannot be AUTH error here
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
ERR err -> do ERR err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
@ -468,11 +468,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure ()
forM_ aChatMsgs $ \case forM_ aChatMsgs $ \case
Right (ACMsg _ chatMsg) -> Right (ACMsg _ chatMsg) ->
processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> toView $ CEvtChatError (Just user) e processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> eToView e
Left e -> do Left e -> do
atomically $ modifyTVar' tags ("error" :) atomically $ modifyTVar' tags ("error" :)
logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e
toView $ CEvtChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) eToView (ChatError . CEException $ "error parsing chat message: " <> e)
checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent
where where
aChatMsgs = parseChatMessages msgBody aChatMsgs = parseChatMessages msgBody
@ -655,14 +655,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processConnMWARN connEntity conn err processConnMWARN connEntity conn err
MERR msgId err -> do MERR msgId err -> do
updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err) updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err)
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
processConnMERR connEntity conn err processConnMERR connEntity conn err
MERRS msgIds err -> do MERRS msgIds err -> do
-- error cannot be AUTH error here -- error cannot be AUTH error here
updateDirectItemsStatusMsgs ct conn (L.toList msgIds) (CISSndError $ agentSndError err) updateDirectItemsStatusMsgs ct conn (L.toList msgIds) (CISSndError $ agentSndError err)
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
ERR err -> do ERR err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
@ -757,7 +757,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
XInfo _ -> XInfo _ ->
-- TODO Keep rejected member to allow them to appeal against rejection. -- TODO Keep rejected member to allow them to appeal against rejection.
when (memberStatus m == GSMemRejected) $ do when (memberStatus m == GSMemRejected) $ do
deleteMemberConnection' user m True deleteMemberConnection' m True
withStore' $ \db -> deleteGroupMember db user m withStore' $ \db -> deleteGroupMember db user m
XOk -> pure () XOk -> pure ()
_ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok" _ -> messageError "INFO from member must have x.grp.mem.info, x.info or x.ok"
@ -831,12 +831,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
forM_ aChatMsgs $ \case forM_ aChatMsgs $ \case
Right (ACMsg _ chatMsg) -> Right (ACMsg _ chatMsg) ->
processEvent tags eInfo chatMsg `catchChatError` \e -> toView $ CEvtChatError (Just user) e processEvent tags eInfo chatMsg `catchChatError` \e -> eToView e
Left e -> do Left e -> do
atomically $ modifyTVar' tags ("error" :) atomically $ modifyTVar' tags ("error" :)
logInfo $ "group msg=error " <> eInfo <> " " <> tshow e logInfo $ "group msg=error " <> eInfo <> " " <> tshow e
toView $ CEvtChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) eToView (ChatError . CEException $ "error parsing chat message: " <> e)
forwardMsgs (rights aChatMsgs) `catchChatError` (toView . CEvtChatError (Just user)) forwardMsgs (rights aChatMsgs) `catchChatError` eToView
checkSendRcpt $ rights aChatMsgs checkSendRcpt $ rights aChatMsgs
where where
aChatMsgs = parseChatMessages msgBody aChatMsgs = parseChatMessages msgBody
@ -965,16 +965,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
MERR msgId err -> do MERR msgId err -> do
withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSError $ agentSndError err) withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSError $ agentSndError err)
-- group errors are silenced to reduce load on UI event log -- group errors are silenced to reduce load on UI event log
-- toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) -- eToView (ChatErrorAgent err $ Just connEntity)
processConnMERR connEntity conn err processConnMERR connEntity conn err
MERRS msgIds err -> do MERRS msgIds err -> do
let newStatus = GSSError $ agentSndError err let newStatus = GSSError $ agentSndError err
-- error cannot be AUTH error here -- error cannot be AUTH error here
withStore' $ \db -> forM_ msgIds $ \msgId -> withStore' $ \db -> forM_ msgIds $ \msgId ->
updateGroupItemsErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure () updateGroupItemsErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure ()
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
ERR err -> do ERR err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
@ -1051,7 +1051,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
withStore' $ \db -> updateSndFileChunkSent db ft msgId withStore' $ \db -> updateSndFileChunkSent db ft msgId
unless (fileStatus == FSCancelled) $ sendFileChunk user ft unless (fileStatus == FSCancelled) $ sendFileChunk user ft
MERR _ err -> do MERR _ err -> do
cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user) cancelSndFileTransfer user ft True >>= mapM_ deleteAgentConnectionAsync
case err of case err of
SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do
ci <- withStore $ \db -> do ci <- withStore $ \db -> do
@ -1070,7 +1070,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- [async agent commands] continuation on receiving JOINED -- [async agent commands] continuation on receiving JOINED
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
ERR err -> do ERR err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
@ -1119,10 +1119,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- [async agent commands] continuation on receiving JOINED -- [async agent commands] continuation on receiving JOINED
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
MERR _ err -> do MERR _ err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
processConnMERR connEntity conn err processConnMERR connEntity conn err
ERR err -> do ERR err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
@ -1131,7 +1131,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case receiveFileChunk ft@RcvFileTransfer {fileId, chunkSize} conn_ meta@MsgMeta {recipient = (msgId, _), integrity} = \case
FileChunkCancel -> FileChunkCancel ->
unless (rcvFileCompleteOrCancelled ft) $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
toView $ CEvtRcvFileSndCancelled user ci ft toView $ CEvtRcvFileSndCancelled user ci ft
FileChunk {chunkNo, chunkBytes = chunk} -> do FileChunk {chunkNo, chunkBytes = chunk} -> do
@ -1157,7 +1157,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
deleteRcvFileChunks db ft deleteRcvFileChunks db ft
getChatItemByFileId db vr user fileId getChatItemByFileId db vr user fileId
toView $ CEvtRcvFileComplete user ci toView $ CEvtRcvFileComplete user ci
forM_ conn_ $ \conn -> deleteAgentConnectionAsync user (aConnId conn) mapM_ (deleteAgentConnectionAsync . aConnId) conn_
RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure ()
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
@ -1171,10 +1171,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- TODO show/log error, other events in contact request -- TODO show/log error, other events in contact request
_ -> pure () _ -> pure ()
MERR _ err -> do MERR _ err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
processConnMERR connEntity conn err processConnMERR connEntity conn err
ERR err -> do ERR err -> do
toView $ CEvtChatError (Just user) (ChatErrorAgent err $ Just connEntity) eToView (ChatErrorAgent err $ Just connEntity)
when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure ()
-- TODO add debugging output -- TODO add debugging output
_ -> pure () _ -> pure ()
@ -1349,7 +1349,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
badRcvFileChunk :: RcvFileTransfer -> String -> CM () badRcvFileChunk :: RcvFileTransfer -> String -> CM ()
badRcvFileChunk ft err = badRcvFileChunk ft err =
unless (rcvFileCompleteOrCancelled ft) $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync
throwChatError $ CEFileRcvChunk err throwChatError $ CEFileRcvChunk err
memberConnectedChatItem :: GroupInfo -> GroupMember -> CM () memberConnectedChatItem :: GroupInfo -> GroupMember -> CM ()
@ -1816,7 +1816,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
ft <- withStore (\db -> getRcvFileTransfer db user fileId) ft <- withStore (\db -> getRcvFileTransfer db user fileId)
unless (rcvFileCompleteOrCancelled ft) $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
toView $ CEvtRcvFileSndCancelled user ci ft toView $ CEvtRcvFileSndCancelled user ci ft
@ -1910,7 +1910,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
then do then do
ft <- withStore (\db -> getRcvFileTransfer db user fileId) ft <- withStore (\db -> getRcvFileTransfer db user fileId)
unless (rcvFileCompleteOrCancelled ft) $ do unless (rcvFileCompleteOrCancelled ft) $ do
cancelRcvFileTransfer user ft >>= mapM_ (deleteAgentConnectionAsync user) cancelRcvFileTransfer user ft >>= mapM_ deleteAgentConnectionAsync
ci <- withStore $ \db -> getChatItemByFileId db vr user fileId ci <- withStore $ \db -> getChatItemByFileId db vr user fileId
toView $ CEvtRcvFileSndCancelled user ci ft toView $ CEvtRcvFileSndCancelled user ci ft
else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id else messageError "x.file.cancel: group member attempted to cancel file of another member" -- shouldn't happen now that query includes group member id
@ -1997,7 +1997,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
then do then do
ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted ct' <- withStore' $ \db -> updateContactStatus db user c CSDeleted
contactConns <- withStore' $ \db -> getContactConnections db vr userId ct' contactConns <- withStore' $ \db -> getContactConnections db vr userId ct'
deleteAgentConnectionsAsync user $ map aConnId contactConns deleteAgentConnectionsAsync $ map aConnId contactConns
forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted
activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted}
let ct'' = ct' {activeConn = activeConn'} :: Contact let ct'' = ct' {activeConn = activeConn'} :: Contact
@ -2006,7 +2006,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
toView $ CEvtContactDeletedByContact user ct'' toView $ CEvtContactDeletedByContact user ct''
else do else do
contactConns <- withStore' $ \db -> getContactConnections db vr userId c contactConns <- withStore' $ \db -> getContactConnections db vr userId c
deleteAgentConnectionsAsync user $ map aConnId contactConns deleteAgentConnectionsAsync $ map aConnId contactConns
withStore $ \db -> deleteContact db user c withStore $ \db -> deleteContact db user c
where where
brokerTs = metaBrokerTs msgMeta brokerTs = metaBrokerTs msgMeta
@ -2447,7 +2447,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist" Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist"
Right reMember -> do Right reMember -> do
GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv
sendGroupMemberMessage user gInfo reMember (XGrpMemFwd (memberInfo m) introInv) (Just introId) $ sendGroupMemberMessage gInfo reMember (XGrpMemFwd (memberInfo m) introInv) (Just introId) $
withStore' $ withStore' $
\db -> updateIntroStatus db introId GMIntroInvForwarded \db -> updateIntroStatus db introId GMIntroInvForwarded
_ -> messageError "x.grp.mem.inv can be only sent by invitee member" _ -> messageError "x.grp.mem.inv can be only sent by invitee member"
@ -2518,7 +2518,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
| senderRole < GRModerator || senderRole < memberRole -> messageError "x.grp.mem.restrict with insufficient member permissions" | senderRole < GRModerator || senderRole < memberRole -> messageError "x.grp.mem.restrict with insufficient member permissions"
| otherwise -> do | otherwise -> do
bm' <- setMemberBlocked bm bm' <- setMemberBlocked bm
toggleNtf user bm' (not blocked) toggleNtf bm' (not blocked)
let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked let ciContent = CIRcvGroupEvent $ RGEMemberBlocked bmId (fromLocalProfile bmp) blocked
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs ciContent ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs ciContent
groupMsgToView gInfo ci groupMsgToView gInfo ci
@ -2592,7 +2592,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
Right member@GroupMember {groupMemberId, memberProfile} -> Right member@GroupMember {groupMemberId, memberProfile} ->
checkRole member $ do checkRole member $ do
-- ? prohibit deleting member if it's the sender - sender should use x.grp.leave -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave
deleteMemberConnection user member deleteMemberConnection member
-- undeleted "member connected" chat item will prevent deletion of member record -- undeleted "member connected" chat item will prevent deletion of member record
deleteOrUpdateMemberRecord user member deleteOrUpdateMemberRecord user member
when withMessages $ deleteMessages member SMDRcv when withMessages $ deleteMessages member SMDRcv
@ -2613,7 +2613,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM ()
xGrpLeave gInfo m msg brokerTs = do xGrpLeave gInfo m msg brokerTs = do
deleteMemberConnection user m deleteMemberConnection m
-- member record is not deleted to allow creation of "member left" chat item -- member record is not deleted to allow creation of "member left" chat item
withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft
ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEMemberLeft) ci <- saveRcvChatItemNoParse user (CDGroupRcv gInfo m) msg brokerTs (CIRcvGroupEvent RGEMemberLeft)

View file

@ -13,6 +13,7 @@ import Control.Concurrent.STM
import Control.Exception (SomeException, catch) import Control.Exception (SomeException, catch)
import Control.Monad.Except import Control.Monad.Except
import Control.Monad.Reader import Control.Monad.Reader
import Data.Aeson (ToJSON (..))
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.TH as JQ
import Data.Bifunctor (first) import Data.Bifunctor (first)
@ -72,13 +73,19 @@ data DBMigrationResult
$(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "DBM") ''DBMigrationResult) $(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "DBM") ''DBMigrationResult)
data APIResponse = APIResponse {remoteHostId :: Maybe RemoteHostId, resp :: ChatResponse} data APIResult r
= APIResult {remoteHostId :: Maybe RemoteHostId, result :: r}
| APIError {remoteHostId :: Maybe RemoteHostId, error :: ChatError}
data APIEvent = APIEvent {remoteHostId :: Maybe RemoteHostId, resp :: ChatEvent} eitherToResult :: Maybe RemoteHostId -> Either ChatError r -> APIResult r
eitherToResult rhId = either (APIError rhId) (APIResult rhId)
{-# INLINE eitherToResult #-}
$(JQ.deriveToJSON defaultJSON ''APIResponse) $(pure [])
$(JQ.deriveToJSON defaultJSON ''APIEvent) instance ToJSON r => ToJSON (APIResult r) where
toEncoding = $(JQ.mkToEncoding (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''APIResult)
toJSON = $(JQ.mkToJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''APIResult)
foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
@ -290,15 +297,14 @@ chatSendCmd :: ChatController -> B.ByteString -> IO JSONByteString
chatSendCmd cc = chatSendRemoteCmd cc Nothing chatSendCmd cc = chatSendRemoteCmd cc Nothing
chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO JSONByteString chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO JSONByteString
chatSendRemoteCmd cc rh s = J.encode . APIResponse rh <$> runReaderT (execChatCommand rh s) cc chatSendRemoteCmd cc rh s = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s) cc
chatRecvMsg :: ChatController -> IO JSONByteString chatRecvMsg :: ChatController -> IO JSONByteString
chatRecvMsg ChatController {outputQ} = json <$> readChatResponse chatRecvMsg ChatController {outputQ} = J.encode . uncurry eitherToResult <$> readChatResponse
where where
json (remoteHostId, resp) = J.encode APIEvent {remoteHostId, resp}
readChatResponse = readChatResponse =
atomically (readTBQueue outputQ) >>= \case atomically (readTBQueue outputQ) >>= \case
(_, CEvtTerminalEvent {}) -> readChatResponse (_, Right CEvtTerminalEvent {}) -> readChatResponse
out -> pure out out -> pure out
chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString

View file

@ -370,16 +370,17 @@ processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteS
processRemoteCommand remoteHostId c cmd s = case cmd of processRemoteCommand remoteHostId c cmd s = case cmd of
SendFile chatName f -> sendFile "/f" chatName f SendFile chatName f -> sendFile "/f" chatName f
SendImage chatName f -> sendFile "/img" chatName f SendImage chatName f -> sendFile "/img" chatName f
_ -> liftRH remoteHostId $ remoteSend c s _ -> chatRemoteSend s
where where
sendFile cmdName chatName (CryptoFile path cfArgs) = do sendFile cmdName chatName (CryptoFile path cfArgs) = do
-- don't encrypt in host if already encrypted locally -- don't encrypt in host if already encrypted locally
CryptoFile path' cfArgs' <- storeRemoteFile remoteHostId (cfArgs $> False) path CryptoFile path' cfArgs' <- storeRemoteFile remoteHostId (cfArgs $> False) path
let f = CryptoFile path' (cfArgs <|> cfArgs') -- use local or host encryption let f = CryptoFile path' (cfArgs <|> cfArgs') -- use local or host encryption
liftRH remoteHostId $ remoteSend c $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f] chatRemoteSend $ B.unwords [cmdName, B.pack (chatNameStr chatName), cryptoFileStr f]
cryptoFileStr CryptoFile {filePath, cryptoArgs} = cryptoFileStr CryptoFile {filePath, cryptoArgs} =
maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs
<> encodeUtf8 (T.pack filePath) <> encodeUtf8 (T.pack filePath)
chatRemoteSend = either throwError pure <=< liftRH remoteHostId . remoteSend c
liftRH :: RemoteHostId -> ExceptT RemoteProtocolError IO a -> CM a liftRH :: RemoteHostId -> ExceptT RemoteProtocolError IO a -> CM a
liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError) liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError)
@ -496,7 +497,7 @@ parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo
parseCtrlAppInfo ctrlAppInfo = do parseCtrlAppInfo ctrlAppInfo = do
liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo
handleRemoteCommand :: (ByteString -> CM' ChatResponse) -> RemoteCrypto -> TBQueue ChatEvent -> HTTP2Request -> CM' () handleRemoteCommand :: (ByteString -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' ()
handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
logDebug "handleRemoteCommand" logDebug "handleRemoteCommand"
liftIO (tryRemoteError' parseRequest) >>= \case liftIO (tryRemoteError' parseRequest) >>= \case
@ -510,7 +511,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque
parseRequest = do parseRequest = do
(rfKN, header, getNext) <- parseDecryptHTTP2Body encryption request reqBody (rfKN, header, getNext) <- parseDecryptHTTP2Body encryption request reqBody
(rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header) (rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header)
replyError = reply . RRChatResponse . CRChatCmdError Nothing replyError = reply . RRChatResponse . RRError
processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM () processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM ()
processCommand user rfKN getNext = \case processCommand user rfKN getNext = \case
RCSend {command} -> lift $ handleSend execChatCommand command >>= reply RCSend {command} -> lift $ handleSend execChatCommand command >>= reply
@ -527,7 +528,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque
send resp send resp
attach sfKN send attach sfKN send
flush flush
Left e -> toView' . CEvtChatError Nothing . ChatErrorRemoteCtrl $ RCEProtocolError e Left e -> eToView' $ ChatErrorRemoteCtrl $ RCEProtocolError e
takeRCStep :: RCStepTMVar a -> CM a takeRCStep :: RCStepTMVar a -> CM a
takeRCStep = liftError' (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar takeRCStep = liftError' (\e -> ChatErrorAgent {agentError = RCP e, connectionEntity_ = Nothing}) . atomically . takeTMVar
@ -549,17 +550,17 @@ tryRemoteError' :: ExceptT RemoteProtocolError IO a -> IO (Either RemoteProtocol
tryRemoteError' = tryAllErrors' (RPEException . tshow) tryRemoteError' = tryAllErrors' (RPEException . tshow)
{-# INLINE tryRemoteError' #-} {-# INLINE tryRemoteError' #-}
handleSend :: (ByteString -> CM' ChatResponse) -> Text -> CM' RemoteResponse handleSend :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM' RemoteResponse
handleSend execChatCommand command = do handleSend execChatCommand command = do
logDebug $ "Send: " <> tshow command logDebug $ "Send: " <> tshow command
-- execChatCommand checks for remote-allowed commands -- execChatCommand checks for remote-allowed commands
-- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper -- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper
RRChatResponse <$> execChatCommand (encodeUtf8 command) RRChatResponse . eitherToResult <$> execChatCommand (encodeUtf8 command)
handleRecv :: Int -> TBQueue ChatEvent -> IO RemoteResponse handleRecv :: Int -> TBQueue (Either ChatError ChatEvent) -> IO RemoteResponse
handleRecv time events = do handleRecv time events = do
logDebug $ "Recv: " <> tshow time logDebug $ "Recv: " <> tshow time
RRChatEvent <$> (timeout time . atomically $ readTBQueue events) RRChatEvent . fmap eitherToResult <$> (timeout time . atomically $ readTBQueue events)
-- TODO this command could remember stored files and return IDs to allow removing files that are not needed. -- TODO this command could remember stored files and return IDs to allow removing files that are not needed.
-- Also, there should be some process removing unused files uploaded to remote host (possibly, all unused files). -- Also, there should be some process removing unused files uploaded to remote host (possibly, all unused files).
@ -614,7 +615,7 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState =
RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState} RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState}
-- | Take a look at emoji of tlsunique, commit pairing, and start session server -- | Take a look at emoji of tlsunique, commit pairing, and start session server
verifyRemoteCtrlSession :: (ByteString -> CM' ChatResponse) -> Text -> CM RemoteCtrlInfo verifyRemoteCtrlSession :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo
verifyRemoteCtrlSession execChatCommand sessCode' = do verifyRemoteCtrlSession execChatCommand sessCode' = do
(sseq, client, ctrlName, sessionCode, vars) <- (sseq, client, ctrlName, sessionCode, vars) <-
chatReadVar remoteCtrlSession >>= \case chatReadVar remoteCtrlSession >>= \case

View file

@ -16,11 +16,11 @@ import Control.Monad.Except
import Control.Monad.Reader import Control.Monad.Reader
import Crypto.Hash (SHA512) import Crypto.Hash (SHA512)
import qualified Crypto.Hash as CH import qualified Crypto.Hash as CH
import Data.Aeson ((.=)) import Data.Aeson (FromJSON (..), ToJSON (..), (.=))
import qualified Data.Aeson as J import qualified Data.Aeson as J
import qualified Data.Aeson.Key as JK import qualified Data.Aeson.Key as JK
import qualified Data.Aeson.KeyMap as JM import qualified Data.Aeson.KeyMap as JM
import Data.Aeson.TH (deriveJSON) import qualified Data.Aeson.TH as JQ
import qualified Data.Aeson.Types as JT import qualified Data.Aeson.Types as JT
import qualified Data.ByteArray as BA import qualified Data.ByteArray as BA
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
@ -42,7 +42,7 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.File (CryptoFile (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..))
import Simplex.Messaging.Crypto.Lazy (LazyByteString) import Simplex.Messaging.Crypto.Lazy (LazyByteString)
import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON, pattern SingleFieldJSONTag, pattern TaggedObjectJSONData, pattern TaggedObjectJSONTag)
import qualified Simplex.Messaging.TMap as TM import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport (TSbChainKeys) import Simplex.Messaging.Transport (TSbChainKeys)
import Simplex.Messaging.Transport.Buffer (getBuffered) import Simplex.Messaging.Transport.Buffer (getBuffered)
@ -64,16 +64,40 @@ data RemoteCommand
deriving (Show) deriving (Show)
data RemoteResponse data RemoteResponse
= RRChatResponse {chatResponse :: ChatResponse} = RRChatResponse {chatResponse :: RRResult ChatResponse}
| RRChatEvent {chatEvent :: Maybe ChatEvent} -- 'Nothing' on poll timeout | RRChatEvent {chatEvent :: Maybe (RRResult ChatEvent)} -- 'Nothing' on poll timeout
| RRFileStored {filePath :: String} | RRFileStored {filePath :: String}
| RRFile {fileSize :: Word32, fileDigest :: FileDigest} -- provides attachment , fileDigest :: FileDigest | RRFile {fileSize :: Word32, fileDigest :: FileDigest} -- provides attachment , fileDigest :: FileDigest
| RRProtocolError {remoteProcotolError :: RemoteProtocolError} -- The protocol error happened on the server side | RRProtocolError {remoteProcotolError :: RemoteProtocolError} -- The protocol error happened on the server side
deriving (Show) deriving (Show)
data RRResult r
= RRResult {result :: r}
| RRError {error :: ChatError}
deriving (Show)
resultToEither :: RRResult r -> Either ChatError r
resultToEither = \case
RRResult r -> Right r
RRError e -> Left e
{-# INLINE resultToEither #-}
eitherToResult :: Either ChatError r -> RRResult r
eitherToResult = either RRError RRResult
{-# INLINE eitherToResult #-}
$(pure [])
-- Force platform-independent encoding as the types aren't UI-visible -- Force platform-independent encoding as the types aren't UI-visible
$(deriveJSON (taggedObjectJSON $ dropPrefix "RC") ''RemoteCommand) instance ToJSON r => ToJSON (RRResult r) where
$(deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse) toEncoding = $(JQ.mkToEncoding (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult)
toJSON = $(JQ.mkToJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult)
instance FromJSON r => FromJSON (RRResult r) where
parseJSON = $(JQ.mkParseJSON (defaultJSON {J.sumEncoding = J.UntaggedValue}) ''RRResult)
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RC") ''RemoteCommand)
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse)
-- * Client side / desktop -- * Client side / desktop
@ -109,16 +133,16 @@ closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClien
-- ** Commands -- ** Commands
remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO ChatResponse remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO (Either ChatError ChatResponse)
remoteSend c cmd = remoteSend c cmd =
sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd} >>= \case sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd} >>= \case
RRChatResponse cr -> pure cr RRChatResponse cr -> pure $ resultToEither cr
r -> badResponse r r -> badResponse r
remoteRecv :: RemoteHostClient -> Int -> ExceptT RemoteProtocolError IO (Maybe ChatEvent) remoteRecv :: RemoteHostClient -> Int -> ExceptT RemoteProtocolError IO (Maybe (Either ChatError ChatEvent))
remoteRecv c ms = remoteRecv c ms =
sendRemoteCommand' c Nothing RCRecv {wait = ms} >>= \case sendRemoteCommand' c Nothing RCRecv {wait = ms} >>= \case
RRChatEvent cEvt_ -> pure cEvt_ RRChatEvent cEvt_ -> pure $ resultToEither <$> cEvt_
r -> badResponse r r -> badResponse r
remoteStoreFile :: RemoteHostClient -> FilePath -> FilePath -> ExceptT RemoteProtocolError IO FilePath remoteStoreFile :: RemoteHostClient -> FilePath -> FilePath -> ExceptT RemoteProtocolError IO FilePath
@ -172,7 +196,7 @@ convertJSON :: PlatformEncoding -> PlatformEncoding -> J.Value -> J.Value
convertJSON _remote@PEKotlin _local@PEKotlin = id convertJSON _remote@PEKotlin _local@PEKotlin = id
convertJSON PESwift PESwift = id convertJSON PESwift PESwift = id
convertJSON PESwift PEKotlin = owsf2tagged convertJSON PESwift PEKotlin = owsf2tagged
convertJSON PEKotlin PESwift = error "unsupported convertJSON: K/S" -- guarded by handshake convertJSON PEKotlin PESwift = Prelude.error "unsupported convertJSON: K/S" -- guarded by handshake
-- | Convert swift single-field sum encoding into tagged/discriminator-field -- | Convert swift single-field sum encoding into tagged/discriminator-field
owsf2tagged :: J.Value -> J.Value owsf2tagged :: J.Value -> J.Value

View file

@ -64,12 +64,14 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing
unless (isMessage cmd) $ echo s unless (isMessage cmd) $ echo s
r <- runReaderT (execChatCommand rh' bs) cc r <- runReaderT (execChatCommand rh' bs) cc
processResp s cmd rh r case r of
Right r' -> processResp cmd rh r'
Left _ -> when (isMessage cmd) $ echo s
printRespToTerminal ct cc False rh r printRespToTerminal ct cc False rh r
startLiveMessage cmd r mapM_ (startLiveMessage cmd) r
where where
echo s = printToTerminal ct [plain s] echo s = printToTerminal ct [plain s]
processResp s cmd rh = \case processResp cmd rh = \case
CRActiveUser u -> case rh of CRActiveUser u -> case rh of
Nothing -> setActive ct "" Nothing -> setActive ct ""
Just rhId -> updateRemoteUser ct u rhId Just rhId -> updateRemoteUser ct u rhId
@ -80,7 +82,6 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c
CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g
CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g
CRChatCmdError _ _ -> when (isMessage cmd) $ echo s
CRCmdOk _ -> case cmd of CRCmdOk _ -> case cmd of
Right APIDeleteUser {} -> setActive ct "" Right APIDeleteUser {} -> setActive ct ""
_ -> pure () _ -> pure ()
@ -132,7 +133,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
updateLiveMessage typedMsg lm = case liveMessageToSend typedMsg lm of updateLiveMessage typedMsg lm = case liveMessageToSend typedMsg lm of
Just sentMsg -> Just sentMsg ->
sendUpdatedLiveMessage cc sentMsg lm True >>= \case sendUpdatedLiveMessage cc sentMsg lm True >>= \case
CRChatItemUpdated {} -> setLiveMessage lm {sentMsg, typedMsg} Right CRChatItemUpdated {} -> setLiveMessage lm {sentMsg, typedMsg}
_ -> do _ -> do
-- TODO print error -- TODO print error
setLiveMessage lm {typedMsg} setLiveMessage lm {typedMsg}
@ -146,10 +147,10 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do
| otherwise = (s <> reverse (c : w), "") | otherwise = (s <> reverse (c : w), "")
startLiveMessage _ _ = pure () startLiveMessage _ _ = pure ()
sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO ChatResponse sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO (Either ChatError ChatResponse)
sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do
let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg
either (CRChatCmdError Nothing) id <$> runExceptT (processChatCommand cmd) `runReaderT` cc runExceptT (processChatCommand cmd) `runReaderT` cc
runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput :: ChatTerminal -> ChatController -> IO ()
runTerminalInput ct cc = withChatTerm ct $ do runTerminalInput ct cc = withChatTerm ct $ do

View file

@ -1,4 +1,5 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
module Simplex.Chat.Terminal.Main where module Simplex.Chat.Terminal.Main where
@ -7,15 +8,13 @@ import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad import Control.Monad
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
import Network.Socket import Network.Socket
import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatEvent (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatError, ChatEvent (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString)
import Simplex.Chat.Core import Simplex.Chat.Core
import Simplex.Chat.Options import Simplex.Chat.Options
import Simplex.Chat.Options.DB import Simplex.Chat.Options.DB
import Simplex.Chat.Terminal import Simplex.Chat.Terminal
import Simplex.Chat.View (ChatResponseEvent, serializeChatResponse, smpProxyModeStr) import Simplex.Chat.View (ChatResponseEvent, smpProxyModeStr)
import Simplex.Messaging.Client (NetworkConfig (..), SocksMode (..)) import Simplex.Messaging.Client (NetworkConfig (..), SocksMode (..))
import System.Directory (getAppUserDataDirectory) import System.Directory (getAppUserDataDirectory)
import System.Exit (exitFailure) import System.Exit (exitFailure)
@ -45,17 +44,15 @@ simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServer
when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do
(_, r) <- atomically . readTBQueue $ outputQ cc (_, r) <- atomically . readTBQueue $ outputQ cc
case r of case r of
CEvtNewChatItems {} -> printResponse r Right CEvtNewChatItems {} -> printResponse r
_ -> when (chatCmdLog == CCLAll) $ printResponse r _ -> when (chatCmdLog == CCLAll) $ printResponse r
sendChatCmdStr cc chatCmd >>= printResponse sendChatCmdStr cc chatCmd >>= printResponse
threadDelay $ chatCmdDelay * 1000000 threadDelay $ chatCmdDelay * 1000000
where where
printResponse :: ChatResponseEvent r => r -> IO () printResponse :: ChatResponseEvent r => Either ChatError r -> IO ()
printResponse r = do printResponse r = do
ts <- getCurrentTime
tz <- getCurrentTimeZone
rh <- readTVarIO $ currentRemoteHost cc rh <- readTVarIO $ currentRemoteHost cc
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r printResponseEvent (rh, Just user) cfg r
welcome :: ChatConfig -> ChatOpts -> IO () welcome :: ChatConfig -> ChatOpts -> IO ()
welcome ChatConfig {presetServers = PresetServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbOptions, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = welcome ChatConfig {presetServers = PresetServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbOptions, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} =

View file

@ -7,6 +7,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Simplex.Chat.Terminal.Output where module Simplex.Chat.Terminal.Output where
@ -146,19 +147,19 @@ withTermLock ChatTerminal {termLock} action = do
runTerminalOutput :: ChatTerminal -> ChatController -> ChatOpts -> IO () runTerminalOutput :: ChatTerminal -> ChatController -> ChatOpts -> IO ()
runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} ChatOpts {markRead} = do runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} ChatOpts {markRead} = do
forever $ do forever $ do
(outputRH, r) <- atomically $ readTBQueue outputQ (outputRH, r_) <- atomically $ readTBQueue outputQ
case r of forM_ r_ $ \case
CEvtNewChatItems u (ci : _) -> when markRead $ markChatItemRead u ci -- At the moment of writing received items are created one at a time CEvtNewChatItems u (ci : _) -> when markRead $ markChatItemRead u ci -- At the moment of writing received items are created one at a time
CEvtChatItemUpdated u ci -> when markRead $ markChatItemRead u ci CEvtChatItemUpdated u ci -> when markRead $ markChatItemRead u ci
CEvtRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId CEvtRemoteHostConnected {remoteHost = RemoteHostInfo {remoteHostId}} -> getRemoteUser remoteHostId
CEvtRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_ CEvtRemoteHostStopped {remoteHostId_} -> mapM_ removeRemoteUser remoteHostId_
_ -> pure () _ -> pure ()
let printEvent = case logFilePath of let printEvent = case logFilePath of
Just path -> if logEventToFile r then logResponse path else printToTerminal ct Just path -> if either (const True) logEventToFile r_ then logResponse path else printToTerminal ct
_ -> printToTerminal ct _ -> printToTerminal ct
liveItems <- readTVarIO showLiveItems liveItems <- readTVarIO showLiveItems
responseString ct cc liveItems outputRH r >>= printEvent responseString ct cc liveItems outputRH r_ >>= printEvent
chatEventNotification ct cc r mapM_ (chatEventNotification ct cc) r_
where where
markChatItemRead u (AChatItem _ _ chat ci@ChatItem {chatDir, meta = CIMeta {itemStatus}}) = markChatItemRead u (AChatItem _ _ chat ci@ChatItem {chatDir, meta = CIMeta {itemStatus}}) =
case (chatDirNtf u chat chatDir (isUserMention ci), itemStatus) of case (chatDirNtf u chat chatDir (isUserMention ci), itemStatus) of
@ -170,7 +171,7 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha
logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s
getRemoteUser rhId = getRemoteUser rhId =
runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case
CRActiveUser {user} -> updateRemoteUser ct user rhId Right CRActiveUser {user} -> updateRemoteUser ct user rhId
cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr
removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct) removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct)
@ -271,15 +272,17 @@ whenCurrUser cc u a = do
where where
sameUser User {userId = uId} = maybe False $ \User {userId} -> userId == uId sameUser User {userId = uId} = maybe False $ \User {userId} -> userId == uId
printRespToTerminal :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> ChatResponse -> IO () printRespToTerminal :: ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> Either ChatError ChatResponse -> IO ()
printRespToTerminal ct cc liveItems outputRH r = responseString ct cc liveItems outputRH r >>= printToTerminal ct printRespToTerminal ct cc liveItems outputRH r = responseString ct cc liveItems outputRH r >>= printToTerminal ct
responseString :: ChatResponseEvent r => ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> r -> IO [StyledString] responseString :: forall r. ChatResponseEvent r => ChatTerminal -> ChatController -> Bool -> Maybe RemoteHostId -> Either ChatError r -> IO [StyledString]
responseString ct cc liveItems outputRH r = do responseString ct cc liveItems outputRH = \case
Right r -> do
cu <- getCurrentUser ct cc cu <- getCurrentUser ct cc
ts <- getCurrentTime ts <- getCurrentTime
tz <- getCurrentTimeZone tz <- getCurrentTimeZone
pure $ responseToView cu (config cc) liveItems ts tz outputRH r pure $ responseToView cu (config cc) liveItems ts tz outputRH r
Left e -> pure $ chatErrorToView (isCommandResponse @r) (config cc) e
updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO () updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO ()
updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct) updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct)

View file

@ -1,3 +1,4 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-} {-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
@ -38,7 +39,6 @@ import Data.Time.Format (defaultTimeLocale, formatTime)
import qualified Data.Version as V import qualified Data.Version as V
import qualified Network.HTTP.Types as Q import qualified Network.HTTP.Types as Q
import Numeric (showFFloat) import Numeric (showFFloat)
import Simplex.Chat (defaultChatConfig)
import Simplex.Chat.Call import Simplex.Chat.Call
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Help import Simplex.Chat.Help
@ -87,15 +87,26 @@ data WCallCommand
$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand) $(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand)
serializeChatResponse :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> String serializeChatError :: Bool -> ChatConfig -> ChatError -> String
serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig False ts tz remoteHost_ serializeChatError isCmd cfg = unlines . map unStyle . chatErrorToView isCmd cfg
serializeChatResponse :: ChatResponseEvent r => (Maybe RemoteHostId, Maybe User) -> ChatConfig -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> String
serializeChatResponse hu cfg ts tz remoteHost_ = unlines . map unStyle . responseToView hu cfg False ts tz remoteHost_
class ChatResponseEvent r where class ChatResponseEvent r where
responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> [StyledString] responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> r -> [StyledString]
isCommandResponse :: Bool
instance ChatResponseEvent ChatResponse where responseToView = chatResponseToView instance ChatResponseEvent ChatResponse where
responseToView = chatResponseToView
isCommandResponse = True
instance ChatResponseEvent ChatEvent where responseToView = chatEventToView instance ChatResponseEvent ChatEvent where
responseToView = chatEventToView
isCommandResponse = False
chatErrorToView :: Bool -> ChatConfig -> ChatError -> [StyledString]
chatErrorToView isCmd ChatConfig {logLevel, testView} = viewChatError isCmd logLevel testView
chatResponseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] chatResponseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString]
chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveItems ts tz outputRH = \case chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveItems ts tz outputRH = \case
@ -286,7 +297,6 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
[ "agent queues info:", [ "agent queues info:",
plain . LB.unpack $ J.encode agentQueuesInfo plain . LB.unpack $ J.encode agentQueuesInfo
] ]
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError True logLevel testView e
CRAppSettings as -> ["app settings: " <> viewJSON as] CRAppSettings as -> ["app settings: " <> viewJSON as]
CRCustomChatResponse u r -> ttyUser' u $ map plain $ T.lines r CRCustomChatResponse u r -> ttyUser' u $ map plain $ T.lines r
where where
@ -296,8 +306,6 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
| otherwise = [] | otherwise = []
ttyUser' :: Maybe User -> [StyledString] -> [StyledString] ttyUser' :: Maybe User -> [StyledString] -> [StyledString]
ttyUser' = maybe id ttyUser ttyUser' = maybe id ttyUser
ttyUserPrefix' :: Maybe User -> [StyledString] -> [StyledString]
ttyUserPrefix' = maybe id $ ttyUserPrefix hu outputRH
testViewChats :: [AChat] -> [StyledString] testViewChats :: [AChat] -> [StyledString]
testViewChats chats = [sShow $ map toChatView chats] testViewChats chats = [sShow $ map toChatView chats]
where where
@ -499,8 +507,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
CEvtAgentConnsDeleted acIds -> ["completed deleting connections: " <> sShow (length acIds) | logLevel <= CLLInfo] CEvtAgentConnsDeleted acIds -> ["completed deleting connections: " <> sShow (length acIds) | logLevel <= CLLInfo]
CEvtAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""] CEvtAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""]
CEvtMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning] CEvtMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning]
CEvtChatError u e -> ttyUser' u $ viewChatError False logLevel testView e CEvtChatErrors errs -> concatMap (viewChatError False logLevel testView) errs
CEvtChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs
CEvtTimedAction _ _ -> [] CEvtTimedAction _ _ -> []
CEvtTerminalEvent te -> case te of CEvtTerminalEvent te -> case te of
TERejectingGroupJoinRequestMember _ g m reason -> [ttyFullMember m <> ": rejecting request to join group " <> ttyGroup' g <> ", reason: " <> sShow reason] TERejectingGroupJoinRequestMember _ g m reason -> [ttyFullMember m <> ": rejecting request to join group " <> ttyGroup' g <> ", reason: " <> sShow reason]

View file

@ -5,55 +5,55 @@ module JSONFixtures where
import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.ByteString.Lazy.Char8 as LB
noActiveUserSwift :: LB.ByteString noActiveUserSwift :: LB.ByteString
noActiveUserSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"noActiveUser\":{}}}}}}}" noActiveUserSwift = "{\"error\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"noActiveUser\":{}}}}}"
noActiveUserTagged :: LB.ByteString noActiveUserTagged :: LB.ByteString
noActiveUserTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}" noActiveUserTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}"
activeUserExistsSwift :: LB.ByteString activeUserExistsSwift :: LB.ByteString
activeUserExistsSwift = "{\"resp\":{\"_owsf\":true,\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"userExists\":{\"contactName\":\"alice\"}}}}}}}" activeUserExistsSwift = "{\"error\":{\"_owsf\":true,\"error\":{\"errorType\":{\"_owsf\":true,\"userExists\":{\"contactName\":\"alice\"}}}}}"
activeUserExistsTagged :: LB.ByteString activeUserExistsTagged :: LB.ByteString
activeUserExistsTagged = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}}" activeUserExistsTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}"
activeUserSwift :: LB.ByteString activeUserSwift :: LB.ByteString
activeUserSwift = "{\"resp\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}" activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}"
activeUserTagged :: LB.ByteString activeUserTagged :: LB.ByteString
activeUserTagged = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}" activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}"
chatStartedSwift :: LB.ByteString chatStartedSwift :: LB.ByteString
chatStartedSwift = "{\"resp\":{\"_owsf\":true,\"chatStarted\":{}}}" chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}"
chatStartedTagged :: LB.ByteString chatStartedTagged :: LB.ByteString
chatStartedTagged = "{\"resp\":{\"type\":\"chatStarted\"}}" chatStartedTagged = "{\"result\":{\"type\":\"chatStarted\"}}"
networkStatusesSwift :: LB.ByteString networkStatusesSwift :: LB.ByteString
networkStatusesSwift = "{\"resp\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}" networkStatusesSwift = "{\"result\":{\"_owsf\":true,\"networkStatuses\":{\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}}"
networkStatusesTagged :: LB.ByteString networkStatusesTagged :: LB.ByteString
networkStatusesTagged = "{\"resp\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}" networkStatusesTagged = "{\"result\":{\"type\":\"networkStatuses\",\"user_\":" <> userJSON <> ",\"networkStatuses\":[]}}"
userJSON :: LB.ByteString userJSON :: LB.ByteString
userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}" userJSON = "{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}"
memberSubSummarySwift :: LB.ByteString memberSubSummarySwift :: LB.ByteString
memberSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}" memberSubSummarySwift = "{\"result\":{\"_owsf\":true,\"memberSubSummary\":{\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}}"
memberSubSummaryTagged :: LB.ByteString memberSubSummaryTagged :: LB.ByteString
memberSubSummaryTagged = "{\"resp\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}" memberSubSummaryTagged = "{\"result\":{\"type\":\"memberSubSummary\",\"user\":" <> userJSON <> ",\"memberSubscriptions\":[]}}"
userContactSubSummarySwift :: LB.ByteString userContactSubSummarySwift :: LB.ByteString
userContactSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}" userContactSubSummarySwift = "{\"result\":{\"_owsf\":true,\"userContactSubSummary\":{\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}}"
userContactSubSummaryTagged :: LB.ByteString userContactSubSummaryTagged :: LB.ByteString
userContactSubSummaryTagged = "{\"resp\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}" userContactSubSummaryTagged = "{\"result\":{\"type\":\"userContactSubSummary\",\"user\":" <> userJSON <> ",\"userContactSubscriptions\":[]}}"
pendingSubSummarySwift :: LB.ByteString pendingSubSummarySwift :: LB.ByteString
pendingSubSummarySwift = "{\"resp\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}" pendingSubSummarySwift = "{\"result\":{\"_owsf\":true,\"pendingSubSummary\":{\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}}"
pendingSubSummaryTagged :: LB.ByteString pendingSubSummaryTagged :: LB.ByteString
pendingSubSummaryTagged = "{\"resp\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}" pendingSubSummaryTagged = "{\"result\":{\"type\":\"pendingSubSummary\",\"user\":" <> userJSON <> ",\"pendingSubscriptions\":[]}}"
parsedMarkdownSwift :: LB.ByteString parsedMarkdownSwift :: LB.ByteString
parsedMarkdownSwift = "{\"formattedText\":[{\"format\":{\"_owsf\":true,\"bold\":{}},\"text\":\"hello\"}]}" parsedMarkdownSwift = "{\"formattedText\":[{\"format\":{\"_owsf\":true,\"bold\":{}},\"text\":\"hello\"}]}"

View file

@ -29,7 +29,7 @@ import Foreign.Storable (peek)
import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding) import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding)
import JSONFixtures import JSONFixtures
import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Controller (ChatController (..))
import Simplex.Chat.Mobile import Simplex.Chat.Mobile hiding (error)
import Simplex.Chat.Mobile.File import Simplex.Chat.Mobile.File
import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.Shared
import Simplex.Chat.Mobile.WebRTC import Simplex.Chat.Mobile.WebRTC