From fc04872c91a54fa9ec765996aafb4b9b1b2ad4ba Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 20 Jun 2025 11:54:21 +0100 Subject: [PATCH] core: chat item content types for chat initiation (#5998) * core: chat item content types for chat initiation * connection mode for ui * padding * simplexmq * initial items * update content items * core: feature and e2e items * refactor * chat items * ios types * fix condition for PQ encryption of link --- apps/ios/Shared/Model/SimpleXAPI.swift | 8 +-- apps/ios/Shared/Views/Chat/ChatItemView.swift | 19 ++++--- .../Chat/ComposeMessage/ComposeView.swift | 7 ++- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +++--- apps/ios/SimpleXChat/ChatTypes.swift | 39 +++++++++++-- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Library/Commands.hs | 55 +++++++++++++------ src/Simplex/Chat/Library/Internal.hs | 40 +++++++++++--- src/Simplex/Chat/Library/Subscriber.hs | 25 ++++----- src/Simplex/Chat/Messages/CIContent.hs | 14 +++-- src/Simplex/Chat/Protocol.hs | 30 +++++++++- src/Simplex/Chat/Store/Connections.hs | 6 +- src/Simplex/Chat/Store/Direct.hs | 10 ++-- src/Simplex/Chat/Store/Groups.hs | 12 ++-- src/Simplex/Chat/Store/Shared.hs | 34 ++++++++---- src/Simplex/Chat/Types.hs | 10 +++- tests/ChatTests/Profiles.hs | 2 +- 19 files changed, 230 insertions(+), 103 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 7ad62c900c..dec083e36c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1350,7 +1350,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool var fileIdsToApprove: [Int64] = [] var srvsToApprove: Set = [] var otherFileErrs: [APIResult] = [] - + for fileId in fileIds { let r: APIResult = await chatApiSendCmd( .receiveFile( @@ -1374,7 +1374,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool otherFileErrs.append(r) } } - + if !auto { let otherErrsStr = fileErrorStrs(otherFileErrs) // If there are not approved files, alert is shown the same way both in case of singular and plural files reception @@ -1439,7 +1439,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool } } } - + func fileErrorStrs(_ errs: [APIResult]) -> String { var errStr = "" if errs.count >= 1 { @@ -1454,7 +1454,7 @@ func receiveFiles(user: any UserLike, fileIds: [Int64], userApprovedRelays: Bool return errStr } } - + func cancelFile(user: User, fileId: Int64) async { if let chatItem = await apiCancelFile(fileId: fileId) { await chatItemSimpleUpdate(user, chatItem) diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index a412bf4452..5f0ab5e329 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -241,16 +241,21 @@ struct ChatItemContentView: View { } private func directE2EEInfoText(_ info: E2EEInfo) -> Text { - info.pqEnabled - ? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.") - .font(.caption) - .foregroundColor(theme.colors.secondary) - .fontWeight(.light) - : e2eeInfoNoPQText() + if let pqEnabled = info.pqEnabled { + pqEnabled + ? e2eeInfoText("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.") + : e2eeInfoNoPQText() + } else { + e2eeInfoText("Messages are protected by **end-to-end encryption**.") + } } private func e2eeInfoNoPQText() -> Text { - Text("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.") + e2eeInfoText("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.") + } + + private func e2eeInfoText(_ s: LocalizedStringKey) -> Text { + Text(s) .font(.caption) .foregroundColor(theme.colors.secondary) .fontWeight(.light) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 1dd945696b..57cf8063d2 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -402,7 +402,7 @@ struct ComposeView: View { case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed default: previewView() } - HStack (alignment: .bottom) { + HStack (alignment: .center) { if !chat.chatInfo.nextConnect { attachmentButton() } @@ -411,7 +411,6 @@ struct ComposeView: View { if chat.chatInfo.nextConnect { nextConnectButton() - .padding(.bottom, 16) .padding(.horizontal, 8) } } @@ -634,7 +633,6 @@ struct ComposeView: View { } .disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled) .frame(width: 25, height: 25) - .padding(.bottom, 16) .tint(theme.colors.primary) if im.secondaryIMFilter == nil, case let .group(g, _) = chat.chatInfo, @@ -1085,6 +1083,9 @@ struct ComposeView: View { return .file(msgText) case .report(_, let reason): return .report(text: msgText, reason: reason) + // TODO [short links] update chat link + case let .chat(_, chatLink): + return .chat(text: msgText, chatLink: chatLink) case .unknown(let type, _): return .unknown(type: type, text: msgText) } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 164f91ae98..d84d449b47 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -543,8 +543,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -704,8 +704,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -790,8 +790,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-LLcXQT7lD7m57CUQwpBZ6W.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-1Rh0qJZPahP19NRjkSYFiJ.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 7cf9d9b7be..b6068f75bd 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1718,7 +1718,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { var createdAt: Date var updatedAt: Date var chatTs: Date? - public var connLinkToConnect: CreatedConnLink? + public var preparedContact: PreparedContact? public var contactRequestId: Int64? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool @@ -1733,9 +1733,9 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } } public var active: Bool { get { contactStatus == .active } } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } - public var nextConnectPrepared: Bool { get { connLinkToConnect != nil && activeConn == nil } } + public var nextConnectPrepared: Bool { get { preparedContact != nil && activeConn == nil } } public var nextAcceptContactRequest: Bool { get { contactRequestId != nil && activeConn == nil } } - public var sendMsgToConnect: Bool { get { nextSendGrpInv || nextConnectPrepared } } + public var sendMsgToConnect: Bool { nextSendGrpInv || preparedContact != nil } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } @@ -1793,6 +1793,16 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { ) } +public struct PreparedContact: Decodable, Hashable { + public var connLinkToConnect: CreatedConnLink + public var uiConnLinkType: ConnectionMode +} + +public enum ConnectionMode: String, Decodable, Hashable { + case inv + case con +} + public enum ContactStatus: String, Decodable, Hashable { case active = "active" case deleted = "deleted" @@ -2191,6 +2201,7 @@ public enum MemberCriteria: String, Codable, Identifiable, Hashable { public struct ContactShortLinkData: Codable, Hashable { public var profile: Profile public var message: String? + public var business: Bool } public struct GroupShortLinkData: Codable, Hashable { @@ -3623,7 +3634,7 @@ public enum CIContent: Decodable, ItemContent, Hashable { } private func directE2EEInfoStr(_ e2eeInfo: E2EEInfo) -> String { - e2eeInfo.pqEnabled + e2eeInfo.pqEnabled == true ? NSLocalizedString("This chat is protected by quantum resistant end-to-end encryption.", comment: "E2EE info chat item") : e2eeInfoNoPQStr } @@ -4104,6 +4115,7 @@ public enum MsgContent: Equatable, Hashable { case voice(text: String, duration: Int) case file(String) case report(text: String, reason: ReportReason) + case chat(text: String, chatLink: MsgChatLink) // TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift case unknown(type: String, text: String) @@ -4116,6 +4128,7 @@ public enum MsgContent: Equatable, Hashable { case let .voice(text, _): return text case let .file(text): return text case let .report(text, _): return text + case let .chat(text, _): return text case let .unknown(_, text): return text } } @@ -4176,6 +4189,7 @@ public enum MsgContent: Equatable, Hashable { case image case duration case reason + case chatLink } public static func == (lhs: MsgContent, rhs: MsgContent) -> Bool { @@ -4187,6 +4201,7 @@ public enum MsgContent: Equatable, Hashable { case let (.voice(lt, ld), .voice(rt, rd)): return lt == rt && ld == rd case let (.file(lf), .file(rf)): return lf == rf case let (.report(lt, lr), .report(rt, rr)): return lt == rt && lr == rr + case let (.chat(lt, ll), .chat(rt, rl)): return lt == rt && ll == rl case let (.unknown(lType, lt), .unknown(rType, rt)): return lType == rType && lt == rt default: return false } @@ -4226,6 +4241,10 @@ extension MsgContent: Decodable { let text = try container.decode(String.self, forKey: CodingKeys.text) let reason = try container.decode(ReportReason.self, forKey: CodingKeys.reason) self = .report(text: text, reason: reason) + case "chat": + let text = try container.decode(String.self, forKey: CodingKeys.text) + let chatLink = try container.decode(MsgChatLink.self, forKey: CodingKeys.chatLink) + self = .chat(text: text, chatLink: chatLink) default: let text = try? container.decode(String.self, forKey: CodingKeys.text) self = .unknown(type: type, text: text ?? "unknown message format") @@ -4267,6 +4286,10 @@ extension MsgContent: Encodable { try container.encode("report", forKey: .type) try container.encode(text, forKey: .text) try container.encode(reason, forKey: .reason) + case let .chat(text, chatLink): + try container.encode("chat", forKey: .type) + try container.encode(text, forKey: .text) + try container.encode(chatLink, forKey: .chatLink) // TODO use original JSON and type case let .unknown(_, text): try container.encode("text", forKey: .type) @@ -4285,6 +4308,12 @@ public enum MsgContentTag: String { case report } +public enum MsgChatLink: Codable, Equatable, Hashable { + case contact(connLink: String, profile: Profile, business: Bool) + case invitation(invLink: String, profile: Profile) + case group(connLink: String, groupProfile: GroupProfile) +} + public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? @@ -4587,7 +4616,7 @@ public enum CIGroupInvitationStatus: String, Decodable, Hashable { } public struct E2EEInfo: Decodable, Hashable { - public var pqEnabled: Bool + public var pqEnabled: Bool? } public enum RcvDirectEvent: Decodable, Hashable { diff --git a/cabal.project b/cabal.project index f1dd7c0514..ccbf63a14f 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: c5b7d3c7afb8c0df8d329885db09b08c2e88109c + tag: af34e80729ea24769d0b707824a4d840f70273cc source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 89b51a9af4..4304a8c523 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."c5b7d3c7afb8c0df8d329885db09b08c2e88109c" = "078bjnw5ypyqlldqy9g49y0y6k3xbg0hckskrg40iw06mc8wj174"; + "https://github.com/simplex-chat/simplexmq.git"."af34e80729ea24769d0b707824a4d840f70273cc" = "0pbkpinm1bwna9f5ix8kimg15fppsqyawn2pijl3xxyjhv0vgwdq"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 52febef824..ba914633f3 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -451,7 +451,7 @@ data ChatCommand | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to | APIConnectPlan UserId AConnectionLink | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData - | APIPrepareGroup UserId ACreatedConnLink GroupShortLinkData + | APIPrepareGroup UserId CreatedLinkContact GroupShortLinkData | APIChangePreparedContactUser ContactId UserId | APIChangePreparedGroupUser GroupId UserId | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 7ae2fc3ca7..a250efe74e 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -99,7 +99,7 @@ import Simplex.Messaging.Compression (compressionLevel) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF -import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Crypto.Ratchet (E2ERatchetParamsUri (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQEncOff, pattern PQSupportOff, pattern PQSupportOn, pqRatchetE2EEncryptVersion) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) @@ -849,6 +849,7 @@ processChatCommand' vr = \case MCVoice {text} -> text /= "" MCFile t -> t /= "" MCReport {} -> True + MCChat {} -> True MCUnknown {} -> True -- TODO [knocking] forward from / to scope APIForwardChatItems toChat@(ChatRef toCType toChatId toScope) fromChat@(ChatRef fromCType fromChatId _fromScope) itemIds itemTTL -> withUser $ \user -> case toCType of @@ -1734,19 +1735,32 @@ processChatCommand' vr = \case pure conn' APIConnectPlan userId cLink -> withUserId userId $ \user -> uncurry (CRConnectionPlan user) <$> connectPlan user cLink - APIPrepareContact userId accLink contactSLinkData -> withUserId userId $ \user -> do - let ContactShortLinkData {profile, message} = contactSLinkData + APIPrepareContact userId accLink@(ACCL cMode (CCLink _ shortLink)) contactSLinkData -> withUserId userId $ \user -> do + let ContactShortLinkData {profile, message, business} = contactSLinkData ct <- withStore $ \db -> createPreparedContact db user profile accLink - forM_ message $ \msg -> - createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent $ MCText msg) Nothing + let cMode' = connMode cMode + createItem content = void $ createInternalItemForChat user (CDDirectRcv ct) content Nothing + msgChatLink = \case + sl@CSLContact {} -> MCLContact sl profile business + sl@CSLInvitation {} -> MCLInvitation sl profile + mapM_ (\sl -> createItem $ CIRcvMsgContent $ MCChat (safeDecodeUtf8 $ strEncode sl) $ msgChatLink sl) shortLink + createItem $ CIRcvDirectE2EEInfo $ E2EInfo $ connLinkPQEncryption accLink + void $ createFeatureEnabledItems_ user ct + mapM_ (createItem . CIRcvMsgContent . MCText) message pure $ CRNewPreparedContact user ct - APIPrepareGroup userId accLink groupSLinkData -> withUserId userId $ \user -> do - let GroupShortLinkData {groupProfile} = groupSLinkData - gInfo <- withStore $ \db -> createPreparedGroup db vr user groupProfile accLink + APIPrepareGroup userId ccLink@(CCLink _ shortLink) groupSLinkData -> withUserId userId $ \user -> do + let GroupShortLinkData {groupProfile = gp@GroupProfile {description}} = groupSLinkData + gInfo <- withStore $ \db -> createPreparedGroup db vr user gp ccLink + -- TODO use received item without member + let cd = CDGroupRcv gInfo Nothing $ membership gInfo + createItem content = void $ createInternalItemForChat user cd content Nothing + mapM_ (\sl -> createItem $ CIRcvMsgContent $ MCChat (safeDecodeUtf8 $ strEncode sl) $ MCLGroup sl gp) shortLink + void $ createGroupFeatureItems_ user cd CIRcvGroupFeature gInfo + mapM_ (createItem . CIRcvMsgContent . MCText) description pure $ CRNewPreparedGroup user gInfo APIChangePreparedContactUser contactId newUserId -> withUser $ \user -> do - ct@Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId - when (isNothing connLinkToConnect) $ throwCmdError "contact doesn't have link to connect" + ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId + when (isNothing preparedContact) $ throwCmdError "contact doesn't have link to connect" when (isJust $ contactConn ct) $ throwCmdError "contact already has connection" newUser <- privateGetUser newUserId ct' <- withFastStore $ \db -> updatePreparedContactUser db vr user ct newUser @@ -1760,10 +1774,10 @@ processChatCommand' vr = \case gInfo' <- withFastStore $ \db -> updatePreparedGroupUser db vr user gInfo hostMember newUser pure $ CRGroupUserChanged user gInfo newUser gInfo' APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do - Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId - case connLinkToConnect of + Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId + case preparedContact of Nothing -> throwCmdError "contact doesn't have link to connect" - Just (ACCL SCMInvitation ccLink) -> + Just PreparedContact {connLinkToConnect = ACCL SCMInvitation ccLink} -> connectViaInvitation user incognito ccLink (Just contactId) >>= \case CRSentConfirmation {customUserProfile} -> do -- get updated contact with connection @@ -1775,7 +1789,7 @@ processChatCommand' vr = \case toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] pure $ CRStartedConnectionToContact user ct' customUserProfile cr -> pure cr - Just (ACCL SCMContact ccLink) -> + Just PreparedContact {connLinkToConnect = ACCL SCMContact ccLink} -> connectViaContact user incognito ccLink msgContent_ (Just $ ACCGContact contactId) >>= \case CRSentInvitation {customUserProfile} -> do -- get updated contact with connection @@ -2031,7 +2045,7 @@ processChatCommand' vr = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing gInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile let cd = CDGroupSnd gInfo Nothing - createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo pure $ CRGroupCreated user gInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> @@ -3438,9 +3452,10 @@ processChatCommand' vr = \case contactShortLinkData :: Profile -> Maybe Text -> CM UserLinkData contactShortLinkData p msg = do large <- chatReadVar useLargeLinkData + -- TODO [short links] business let contactData - | large = ContactShortLinkData p msg - | otherwise = ContactShortLinkData p {fullName = "", image = Nothing, contactLink = Nothing} Nothing + | large = ContactShortLinkData p msg False + | otherwise = ContactShortLinkData p {fullName = "", image = Nothing, contactLink = Nothing} Nothing False pure $ encodeShortLinkData large contactData groupShortLinkData :: GroupProfile -> CM UserLinkData groupShortLinkData gp = do @@ -4441,7 +4456,7 @@ chatCommandP = "/contacts" $> ListContacts, "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP), "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), - "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), + "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP' <* A.space <*> jsonP), "/_set contact user @" *> (APIChangePreparedContactUser <$> A.decimal <* A.space <*> A.decimal), "/_set group user #" *> (APIChangePreparedGroupUser <$> A.decimal <* A.space <*> A.decimal), "/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)), @@ -4561,6 +4576,10 @@ chatCommandP = (ACR m cReq) <- strP sLink_ <- optional (A.space *> strP) pure $ ACCL m (CCLink cReq sLink_) + connLinkP' = do + cReq <- strP + sLink_ <- optional (A.space *> strP) + pure $ CCLink cReq sLink_ connLinkP_ = ((Just <$> connLinkP) <|> A.takeTill (== ' ') $> Nothing) incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index a42b1658ff..e0186aad94 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -316,6 +316,7 @@ quoteContent mc qmc ciFile_ MCVideo {} -> True MCVoice {} -> False MCReport {} -> False + MCChat {} -> True MCUnknown {} -> True qText = msgContentText qmc getFileName :: CIFile d -> String @@ -1019,7 +1020,7 @@ acceptBusinessJoinRequestAsync connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV withStore' $ \db -> createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode let cd = CDGroupSnd gInfo Nothing - createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo pure (gInfo, clientMember) where @@ -1265,7 +1266,7 @@ createContactPQSndItem :: User -> Contact -> Connection -> PQEncryption -> CM (C createContactPQSndItem user ct conn@Connection {pqSndEnabled} pqSndEnabled' = flip catchChatError (const $ pure (ct, conn)) $ case (pqSndEnabled, pqSndEnabled') of (Just b, b') | b' /= b -> createPQItem $ CISndConnEvent (SCEPqEnabled pqSndEnabled') - (Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (E2EInfo pqSndEnabled') + (Nothing, PQEncOn) -> createPQItem $ CISndDirectE2EEInfo (E2EInfo $ Just pqSndEnabled') _ -> pure (ct, conn) where createPQItem ciContent = do @@ -1280,7 +1281,7 @@ updateContactPQRcv :: User -> Contact -> Connection -> PQEncryption -> CM (Conta updateContactPQRcv user ct conn@Connection {connId, pqRcvEnabled} pqRcvEnabled' = flip catchChatError (const $ pure (ct, conn)) $ case (pqRcvEnabled, pqRcvEnabled') of (Just b, b') | b' /= b -> updatePQ $ CIRcvConnEvent (RCEPqEnabled pqRcvEnabled') - (Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (E2EInfo pqRcvEnabled') + (Nothing, PQEncOn) -> updatePQ $ CIRcvDirectE2EEInfo (E2EInfo $ Just pqRcvEnabled') _ -> pure (ct, conn) where updatePQ ciContent = do @@ -2261,6 +2262,12 @@ userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do let userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs} +connLinkPQEncryption :: ACreatedConnLink -> Maybe PQEncryption +connLinkPQEncryption (ACCL _ (CCLink cReq _)) = case cReq of + CRContactUri _ -> Nothing + CRInvitationUri _ (CR.E2ERatchetParamsUri vr' _ _ pq) -> + Just $ PQEncryption $ maxVersion vr' >= CR.pqRatchetE2EEncryptVersion && isJust pq + createRcvFeatureItems :: User -> Contact -> Contact -> CM' () createRcvFeatureItems user ct ct' = createFeatureItems user ct ct' CDDirectRcv CIRcvChatFeature CIRcvChatPreference contactPreference @@ -2275,6 +2282,15 @@ createSndFeatureItems user ct ct' = type FeatureContent a d = ChatFeature -> a -> Maybe Int -> CIContent d +createFeatureEnabledItems :: User -> Contact -> CM () +createFeatureEnabledItems user ct = createFeatureEnabledItems_ user ct >>= toView . CEvtNewChatItems user + +createFeatureEnabledItems_ :: User -> Contact -> CM [AChatItem] +createFeatureEnabledItems_ user ct@Contact {mergedPreferences} = + forM allChatFeatures $ \(ACF f) -> do + let state = featureState $ getContactUserPreference f mergedPreferences + createInternalItemForChat user (CDDirectRcv ct) (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing + createFeatureItems :: MsgDirectionI d => User -> @@ -2337,16 +2353,24 @@ sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing} createGroupFeatureItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> CM () -createGroupFeatureItems user cd ciContent GroupInfo {fullGroupPreferences} = - forM_ allGroupFeatures $ \(AGF f) -> do +createGroupFeatureItems user cd ciContent g = createGroupFeatureItems_ user cd ciContent g >>= toView . CEvtNewChatItems user + +createGroupFeatureItems_ :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> CM [AChatItem] +createGroupFeatureItems_ user cd ciContent GroupInfo {fullGroupPreferences} = + forM allGroupFeatures $ \(AGF f) -> do let p = getGroupPreference f fullGroupPreferences (_, param, role) = groupFeatureState p - createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing + createInternalItemForChat user cd (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM () -createInternalChatItem user cd content itemTs_ = +createInternalChatItem user cd content itemTs_ = do + ci <- createInternalItemForChat user cd content itemTs_ + toView $ CEvtNewChatItems user [ci] + +createInternalItemForChat :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM AChatItem +createInternalItemForChat user cd content itemTs_ = lift (createInternalItemsForChats user itemTs_ [(cd, [content])]) >>= \case - [Right aci] -> toView $ CEvtNewChatItems user [aci] + [Right ci] -> pure ci [Left e] -> throwError e rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs) diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 189378f4e6..a6806c9c9e 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -556,8 +556,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO check member ID -- TODO update member profile pure () - XInfo profile -> - void $ processContactProfileUpdate ct profile False + XInfo profile -> do + let prepared = isJust $ preparedContact ct + void $ processContactProfileUpdate ct profile prepared XOk -> pure () _ -> messageError "INFO for existing contact must have x.grp.mem.info, x.info or x.ok" CON pqEnc -> @@ -570,9 +571,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) lift $ setContactNetworkStatus ct' NSConnected toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile) - when (directOrUsed ct') $ do - createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo pqEnc) Nothing - createFeatureEnabledItems ct' + let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo $ Just pqEnc) Nothing + when (directOrUsed ct') $ case preparedContact ct' of + Nothing -> do + createE2EItem + createFeatureEnabledItems user ct' + Just PreparedContact {connLinkToConnect = cl} -> + unless (Just pqEnc == connLinkPQEncryption cl) createE2EItem when (contactConnInitiated conn') $ do let Connection {groupLinkId} = conn' doProbeContacts = isJust groupLinkId @@ -799,7 +804,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtUserJoinedGroup user gInfo' m' (gInfo'', m'', scopeInfo) <- mkGroupChatScope gInfo' m' let cd = CDGroupRcv gInfo'' scopeInfo m'' - createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing createGroupFeatureItems user cd CIRcvGroupFeature gInfo'' memberConnectedChatItem gInfo'' scopeInfo m'' unless (memberPending membership) $ maybeCreateGroupDescrLocal gInfo'' m'' @@ -2181,7 +2186,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- create item in both scopes let gInfo' = gInfo {membership = membership'} cd = CDGroupRcv gInfo' Nothing m - createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing + createInternalChatItem user cd (CIRcvGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing createGroupFeatureItems user cd CIRcvGroupFeature gInfo' maybeCreateGroupDescrLocal gInfo' m createInternalChatItem user cd (CIRcvGroupEvent RGEUserAccepted) Nothing @@ -2250,12 +2255,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let ciContent = CIRcvGroupEvent $ RGEMemberProfileUpdated (fromLocalProfile p) p' createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m'') ciContent itemTs_ - createFeatureEnabledItems :: Contact -> CM () - createFeatureEnabledItems ct@Contact {mergedPreferences} = - forM_ allChatFeatures $ \(ACF f) -> do - let state = featureState $ getContactUserPreference f mergedPreferences - createInternalChatItem user (CDDirectRcv ct) (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing - xInfoProbe :: ContactOrMember -> Probe -> CM () xInfoProbe cgm2 probe = do contactMerge <- readTVarIO =<< asks contactMergeEnabled diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index cc6529831c..09d1e9a281 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -173,7 +173,7 @@ data CIContent (d :: MsgDirection) where deriving instance Show (CIContent d) -data E2EInfo = E2EInfo {pqEnabled :: PQEncryption} +data E2EInfo = E2EInfo {pqEnabled :: Maybe PQEncryption} deriving (Eq, Show) ciMsgContent :: CIContent d -> Maybe MsgContent @@ -296,11 +296,17 @@ ciContentToText = \case directE2EInfoToText :: E2EInfo -> Text directE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of - PQEncOn -> e2eInfoPQText - PQEncOff -> e2eInfoNoPQText + Just PQEncOn -> e2eInfoPQText + Just PQEncOff -> e2eInfoNoPQText + Nothing -> simpleE2EText groupE2EInfoToText :: E2EInfo -> Text -groupE2EInfoToText _e2eeInfo = e2eInfoNoPQText +groupE2EInfoToText E2EInfo {pqEnabled} = case pqEnabled of + Just _ -> e2eInfoNoPQText + Nothing -> simpleE2EText + +simpleE2EText :: Text +simpleE2EText = "This conversation is protected by end-to-end encryption" e2eInfoNoPQText :: Text e2eInfoNoPQText = diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index b206cec6fb..b5e85eebf1 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -533,7 +533,16 @@ cmToQuotedMsg = \case ACME _ (XMsgNew (MCQuote quotedMsg _)) -> Just quotedMsg _ -> Nothing -data MsgContentTag = MCText_ | MCLink_ | MCImage_ | MCVideo_ | MCVoice_ | MCFile_ | MCReport_ | MCUnknown_ Text +data MsgContentTag + = MCText_ + | MCLink_ + | MCImage_ + | MCVideo_ + | MCVoice_ + | MCFile_ + | MCReport_ + | MCChat_ + | MCUnknown_ Text deriving (Eq, Show) instance StrEncoding MsgContentTag where @@ -545,6 +554,7 @@ instance StrEncoding MsgContentTag where MCFile_ -> "file" MCVoice_ -> "voice" MCReport_ -> "report" + MCChat_ -> "chat" MCUnknown_ t -> encodeUtf8 t strDecode = \case "text" -> Right MCText_ @@ -554,6 +564,7 @@ instance StrEncoding MsgContentTag where "voice" -> Right MCVoice_ "file" -> Right MCFile_ "report" -> Right MCReport_ + "chat" -> Right MCChat_ t -> Right . MCUnknown_ $ safeDecodeUtf8 t strP = strDecode <$?> A.takeTill (== ' ') @@ -593,9 +604,16 @@ data MsgContent | MCVoice {text :: Text, duration :: Int} | MCFile {text :: Text} | MCReport {text :: Text, reason :: ReportReason} + | MCChat {text :: Text, chatLink :: MsgChatLink} | MCUnknown {tag :: Text, text :: Text, json :: J.Object} deriving (Eq, Show) +data MsgChatLink + = MCLContact {connLink :: ShortLinkContact, profile :: Profile, business :: Bool} + | MCLInvitation {invLink :: ShortLinkInvitation, profile :: Profile} + | MCLGroup {connLink :: ShortLinkContact, groupProfile :: GroupProfile} + deriving (Eq, Show) + msgContentText :: MsgContent -> Text msgContentText = \case MCText t -> t @@ -611,6 +629,7 @@ msgContentText = \case if T.null text then msg else msg <> ": " <> text where msg = "report " <> safeDecodeUtf8 (strEncode reason) + MCChat {text} -> text MCUnknown {text} -> text durationText :: Int -> Text @@ -646,6 +665,7 @@ msgContentTag = \case MCVoice {} -> MCVoice_ MCFile {} -> MCFile_ MCReport {} -> MCReport_ + MCChat {} -> MCChat_ MCUnknown {tag} -> MCUnknown_ tag data ExtMsgContent = ExtMsgContent @@ -664,6 +684,8 @@ data ExtMsgContent = ExtMsgContent data MsgMention = MsgMention {memberId :: MemberId} deriving (Eq, Show) +$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "MCL") ''MsgChatLink) + $(JQ.deriveJSON defaultJSON ''MsgMention) $(JQ.deriveJSON defaultJSON ''QuotedMsg) @@ -773,6 +795,10 @@ instance FromJSON MsgContent where text <- v .: "text" reason <- v .: "reason" pure MCReport {text, reason} + MCChat_ -> do + text <- v .: "text" + chatLink <- v .: "chatLink" + pure MCChat {text, chatLink} MCUnknown_ tag -> do text <- fromMaybe unknownMsgType <$> v .:? "text" pure MCUnknown {tag, text, json = v} @@ -807,6 +833,7 @@ instance ToJSON MsgContent where MCVoice {text, duration} -> J.object ["type" .= MCVoice_, "text" .= text, "duration" .= duration] MCFile t -> J.object ["type" .= MCFile_, "text" .= t] MCReport {text, reason} -> J.object ["type" .= MCReport_, "text" .= text, "reason" .= reason] + MCChat {text, chatLink} -> J.object ["type" .= MCChat_, "text" .= text, "chatLink" .= chatLink] toEncoding = \case MCUnknown {json} -> JE.value $ J.Object json MCText t -> J.pairs $ "type" .= MCText_ <> "text" .= t @@ -816,6 +843,7 @@ instance ToJSON MsgContent where MCVoice {text, duration} -> J.pairs $ "type" .= MCVoice_ <> "text" .= text <> "duration" .= duration MCFile t -> J.pairs $ "type" .= MCFile_ <> "text" .= t MCReport {text, reason} -> J.pairs $ "type" .= MCReport_ <> "text" .= text <> "reason" .= reason + MCChat {text, chatLink} -> J.pairs $ "type" .= MCChat_ <> "text" .= text <> "chatLink" .= chatLink instance ToField MsgContent where toField = toField . encodeJSON diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 38f83283ed..c8359125fa 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -125,8 +125,8 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn - connLinkToConnect = toACreatedConnLink_ connFullLink connShortLink - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + preparedContact = toPreparedContact connFullLink connShortLink + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = do gm <- @@ -218,7 +218,7 @@ getConnectionEntityViaShortLink db vr user@User {userId} shortLink = fmap either (cReq, connId) <- ExceptT getConnReqConnId (cReq,) <$> getConnectionEntity db vr user connId where - getConnReqConnId = + getConnReqConnId = firstRow' toConnReqConnId (SEInternalError "connection not found") $ DB.query db diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index c42d581849..679c1368d4 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -104,7 +104,7 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ACreatedConnLink, ConnId, CreatedConnLink (..), InvitationId, UserId) +import Simplex.Messaging.Agent.Protocol (ACreatedConnLink (..), ConnId, CreatedConnLink (..), InvitationId, UserId, connMode) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -263,7 +263,7 @@ createIncognitoProfile db User {userId} p = do createIncognitoProfile_ db userId createdAt p createPreparedContact :: DB.Connection -> User -> Profile -> ACreatedConnLink -> ExceptT StoreError IO Contact -createPreparedContact db user@User {userId} p@Profile {preferences} connLinkToConnect = do +createPreparedContact db user@User {userId} p@Profile {preferences} connLinkToConnect@(ACCL m _) = do currentTs <- liftIO getCurrentTime (localDisplayName, contactId, profileId) <- createContact_ db userId p (Just connLinkToConnect) "" Nothing currentTs let profile = toLocalProfile profileId p "" @@ -284,7 +284,7 @@ createPreparedContact db user@User {userId} p@Profile {preferences} connLinkToCo createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, - connLinkToConnect = Just connLinkToConnect, + preparedContact = Just $ PreparedContact connLinkToConnect $ connMode m, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, @@ -347,7 +347,7 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, - connLinkToConnect = Nothing, + preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, @@ -931,7 +931,7 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, - connLinkToConnect = Nothing, + preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 396255cffe..3b7651debd 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -172,7 +172,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ACreatedConnLink, ConnId, CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -579,7 +579,7 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) -createPreparedGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> ACreatedConnLink -> ExceptT StoreError IO GroupInfo +createPreparedGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> CreatedLinkContact -> ExceptT StoreError IO GroupInfo createPreparedGroup db vr user@User {userId, userContactId} groupProfile connLinkToConnect = do currentTs <- liftIO getCurrentTime (groupId, groupLDN) <- createGroup_ db userId groupProfile (Just connLinkToConnect) Nothing currentTs @@ -781,7 +781,7 @@ createGroupViaLink' ) insertedRowId db -createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe ACreatedConnLink -> Maybe BusinessChatInfo -> UTCTime -> ExceptT StoreError IO (GroupId, Text) +createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe CreatedLinkContact -> Maybe BusinessChatInfo -> UTCTime -> ExceptT StoreError IO (GroupId, Text) createGroup_ db userId groupProfile connLinkToConnect business currentTs = ExceptT $ do let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do @@ -800,7 +800,7 @@ createGroup_ db userId groupProfile connLinkToConnect business currentTs = Excep business_chat, business_member_id, customer_member_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. connLinkToConnectRow connLinkToConnect :. businessChatInfoRow business) + ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. connLinkToConnectRow' connLinkToConnect :. businessChatInfoRow business) groupId <- insertedRowId db pure (groupId, localDisplayName) @@ -2525,7 +2525,7 @@ createMemberContact quotaErrCounter = 0 } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact db vr user contactId = do @@ -2562,7 +2562,7 @@ createMemberContactInvited contactId <- createContactUpdateMember currentTs userPreferences ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} + mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} m' = m {memberContactId = Just contactId} pure (mCt', m') where diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 71c4b07c18..a01172f211 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -30,6 +30,7 @@ import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) +import Data.Type.Equality import Simplex.Chat.Messages import Simplex.Chat.Protocol import Simplex.Chat.Remote.Types @@ -37,7 +38,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), AConnShortLink (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), SConnectionMode (..), UserId) +import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), AConnShortLink (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink (..), UserId, connMode) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -396,13 +397,21 @@ createContact_ db userId Profile {displayName, fullName, image, contactLink, pre contactId <- insertedRowId db pure $ Right (ldn, contactId, profileId) -type ConnLinkToConnectRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink) +type AConnLinkToConnectRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink) -connLinkToConnectRow :: Maybe ACreatedConnLink -> ConnLinkToConnectRow +connLinkToConnectRow :: Maybe ACreatedConnLink -> AConnLinkToConnectRow connLinkToConnectRow = \case Just (ACCL m (CCLink fullLink shortLink)) -> (Just (ACR m fullLink), ACSL m <$> shortLink) Nothing -> (Nothing, Nothing) +type ConnLinkToConnectRow m = (Maybe (ConnectionRequestUri m), Maybe (ConnShortLink m)) + +connLinkToConnectRow' :: Maybe (CreatedConnLink m) -> ConnLinkToConnectRow m +connLinkToConnectRow' = \case + Just (CCLink fullLink shortLink) -> (Just fullLink, shortLink) + Nothing -> (Nothing, Nothing) +{-# INLINE connLinkToConnectRow' #-} + deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO () deleteUnusedIncognitoProfileById_ db User {userId} profileId = DB.execute @@ -432,17 +441,18 @@ toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, via chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito - connLinkToConnect = toACreatedConnLink_ connFullLink connShortLink - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + preparedContact = toPreparedContact connFullLink connShortLink + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + +toPreparedContact :: Maybe AConnectionRequestUri -> Maybe AConnShortLink -> Maybe PreparedContact +toPreparedContact connFullLink connShortLink = + (\cl@(ACCL m _) -> PreparedContact cl $ connMode m) <$> toACreatedConnLink_ connFullLink connShortLink toACreatedConnLink_ :: Maybe AConnectionRequestUri -> Maybe AConnShortLink -> Maybe ACreatedConnLink -toACreatedConnLink_ connFullLink connShortLink = case (connFullLink, connShortLink) of - (Nothing, _) -> Nothing - (Just (ACR m cr), Nothing) -> Just $ ACCL m (CCLink cr Nothing) - (Just (ACR m cr), Just (ACSL m' l)) -> case (m, m') of - (SCMInvitation, SCMInvitation) -> Just $ ACCL SCMInvitation (CCLink cr (Just l)) - (SCMContact, SCMContact) -> Just $ ACCL SCMContact (CCLink cr (Just l)) - _ -> Nothing +toACreatedConnLink_ Nothing _ = Nothing +toACreatedConnLink_ (Just (ACR m cr)) csl = case csl of + Nothing -> Just $ ACCL m $ CCLink cr Nothing + Just (ACSL m' l) -> (\Refl -> ACCL m $ CCLink cr (Just l)) <$> testEquality m m' getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById db userId profileId = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e5f494b6d8..aa8249fa7f 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -188,7 +188,7 @@ data Contact = Contact createdAt :: UTCTime, updatedAt :: UTCTime, chatTs :: Maybe UTCTime, - connLinkToConnect :: Maybe ACreatedConnLink, + preparedContact :: Maybe PreparedContact, contactRequestId :: Maybe Int64, contactGroupMemberId :: Maybe GroupMemberId, contactGrpInvSent :: Bool, @@ -200,6 +200,9 @@ data Contact = Contact } deriving (Eq, Show) +data PreparedContact = PreparedContact {connLinkToConnect :: ACreatedConnLink, uiConnLinkType :: ConnectionMode} + deriving (Eq, Show) + newtype CustomData = CustomData J.Object deriving (Eq, Show) @@ -658,7 +661,8 @@ deriving newtype instance FromField ImageData data ContactShortLinkData = ContactShortLinkData { profile :: Profile, - message :: Maybe Text + message :: Maybe Text, + business :: Bool } deriving (Show) @@ -1978,6 +1982,8 @@ $(JQ.deriveJSON defaultJSON ''XFTPSndFile) $(JQ.deriveJSON defaultJSON ''FileTransferMeta) +$(JQ.deriveJSON defaultJSON ''PreparedContact) + $(JQ.deriveJSON defaultJSON ''LocalFileMeta) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FT") ''FileTransfer) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 171496d8b8..fada9627d5 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -3530,7 +3530,7 @@ testShortLinkAddressChangeAutoReply = bobContactSLinkData <- getTermLine bob bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> bobContactSLinkData) bob <## "alice: contact is prepared" - bob <# "alice> welcome!" + -- bob <# "alice> welcome!" -- this message is not sent as event bob ##> "/_connect contact @2 text hello" bob <### [ "alice: connection started",