mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 20:29:53 +00:00
core: pass event and response error without dedicated constructor (#5869)
* core: pass event and response error without dedicated constructor * ios: WIP * android, desktop: update UI for new API * ios: fix parser * fix showing invalid chats * fix mobile api tests * ios: split ChatResponse to 3 enums, decode API results on the same thread * tweak types * remove throws * rename
This commit is contained in:
parent
a0d1cca389
commit
24b0f0290b
54 changed files with 2131 additions and 2177 deletions
|
@ -54,7 +54,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||||
try await apiVerifyToken(token: token, nonce: nonce, code: verification)
|
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))")
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
@ -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 &&
|
||||||
|
|
|
@ -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)!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))"))
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)):
|
||||||
|
|
|
@ -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))
|
||||||
//}
|
//}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -48,7 +48,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose
|
||||||
val prefPerformLA = chatModel.controller.appPrefs.performLA.get()
|
val 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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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 ()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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_}}} =
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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\"}]}"
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue