From 6f21826579284b00f3b6ebc59caf5a56bb3ecf3c Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 11 Apr 2026 19:40:33 +0100 Subject: [PATCH] core, ui: chat item to show message error (#6785) * core: chat item to show message error * ui: chat item for removed messages * remove local maven repo * command to test dropped messages * update nix config * show parse errors * error texts, simplexmq * alert messages * simplexmq, alert * better parsing * better parsing * simplify * correct message * remove test api * do not check size twice, bot types * send error in relays * do not create error item in relays * diff --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- .../ChatItem/IntegrityErrorItemView.swift | 18 ++++++++++ apps/ios/Shared/Views/Chat/ChatItemView.swift | 1 + .../Views/Helpers/ChatItemClipShape.swift | 1 + apps/ios/SimpleXChat/ChatTypes.swift | 18 ++++++++++ .../chat/simplex/common/model/ChatModel.kt | 15 +++++++++ .../common/views/chat/item/ChatItemView.kt | 4 +++ .../views/chat/item/IntegrityErrorItemView.kt | 14 ++++++++ .../commonMain/resources/MR/base/strings.xml | 4 +++ bots/api/TYPES.md | 31 +++++++++++++++++ bots/src/API/Docs/Types.hs | 4 +++ cabal.project | 2 +- .../types/typescript/src/types.ts | 33 +++++++++++++++++++ scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Library/Subscriber.hs | 18 ++++++++-- src/Simplex/Chat/Messages/CIContent.hs | 22 +++++++++++++ src/Simplex/Chat/Protocol.hs | 8 +++-- src/Simplex/Chat/View.hs | 8 ++++- 17 files changed, 196 insertions(+), 7 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index fdf3743aac..c816770c76 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -77,6 +77,24 @@ struct CIMsgError: View { } } +struct RcvMsgErrorItemView: View { + @ObservedObject var chat: Chat + var rcvMsgError: RcvMsgError + var chatItem: ChatItem + + var body: some View { + CIMsgError(chat: chat, chatItem: chatItem) { + AlertManager.shared.showAlertMsg( + title: "Message error", + message: switch rcvMsgError { + case let .dropped(attempts): "The app removed this message after \(attempts) attempts to receive it." + case let .parseError(parseError): "\(parseError)" + } + ) + } + } +} + struct IntegrityErrorItemView_Previews: PreviewProvider { static var previews: some View { IntegrityErrorItemView(chat: Chat.sampleData, msgError: .msgBadHash, chatItem: ChatItem.getIntegrityErrorSample()) diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 138aed6c65..d0ff1934ba 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -145,6 +145,7 @@ struct ChatItemContentView: View { } else { ZStack {} } + case let .rcvMsgError(rcvMsgError): RcvMsgErrorItemView(chat: chat, rcvMsgError: rcvMsgError, chatItem: chatItem) case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(chat: chat, msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem) case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift index 980308f13c..0491b38575 100644 --- a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift +++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift @@ -37,6 +37,7 @@ struct ChatItemClipped: ViewModifier { .rcvMsgContent, .rcvDecryptionError, .rcvIntegrityError, + .rcvMsgError, .invalidJSON: let tail = if let mc = ci.content.msgContent, mc.isImageOrVideo && mc.text.isEmpty { false diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 4a14d3ae99..99fdeebac4 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3271,6 +3271,7 @@ public struct ChatItem: Identifiable, Decodable, Hashable { case .rcvCall: return false // notification is shown on .callInvitation instead case .rcvIntegrityError: return false case .rcvDecryptionError: return false + case .rcvMsgError: return false case .rcvGroupInvitation: return true case .sndGroupInvitation: return false case .rcvDirectEvent(rcvDirectEvent: let rcvDirectEvent): @@ -4028,6 +4029,7 @@ public enum CIContent: Decodable, ItemContent, Hashable { case rcvCall(status: CICallStatus, duration: Int) case rcvIntegrityError(msgError: MsgErrorType) case rcvDecryptionError(msgDecryptError: MsgDecryptError, msgCount: UInt32) + case rcvMsgError(rcvMsgError: RcvMsgError) case rcvGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole) case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole) case rcvDirectEvent(rcvDirectEvent: RcvDirectEvent) @@ -4065,6 +4067,7 @@ public enum CIContent: Decodable, ItemContent, Hashable { case let .rcvCall(status, duration): return status.text(duration) case let .rcvIntegrityError(msgError): return msgError.text case let .rcvDecryptionError(msgDecryptError, _): return msgDecryptError.text + case let .rcvMsgError(rcvMsgError): return rcvMsgError.text case let .rcvGroupInvitation(groupInvitation, _): return groupInvitation.text case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text case let .rcvDirectEvent(rcvDirectEvent): return rcvDirectEvent.text @@ -4156,6 +4159,7 @@ public enum CIContent: Decodable, ItemContent, Hashable { case .rcvCall: return true case .rcvIntegrityError: return true case .rcvDecryptionError: return true + case .rcvMsgError: return true case .rcvGroupInvitation: return true case .rcvModerated: return true case .rcvBlocked: return true @@ -5082,6 +5086,20 @@ public enum MsgErrorType: Decodable, Hashable { } } +public enum RcvMsgError: Decodable, Hashable { + case dropped(attempts: Int) + case parseError(parseError: String) + + var text: String { + switch self { + case let .dropped(attempts): + String.localizedStringWithFormat(NSLocalizedString("removed (%d attempts)", comment: "receive error chat item"), attempts) + case let .parseError(parseError): + String.localizedStringWithFormat(NSLocalizedString("error: %@", comment: "receive error chat item"), parseError) + } + } +} + public struct CIGroupInvitation: Decodable, Hashable { public var groupId: Int64 public var groupMemberId: Int64 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 4e406044e5..f89a599bb4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -3040,6 +3040,7 @@ data class ChatItem ( is CIContent.RcvCall -> false // notification is shown on CallInvitation instead is CIContent.RcvIntegrityError -> false is CIContent.RcvDecryptionError -> false + is CIContent.RcvMsgErrorContent -> false is CIContent.RcvGroupInvitation -> true is CIContent.SndGroupInvitation -> false is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) { @@ -3734,6 +3735,7 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvIntegrityError") class RcvIntegrityError(val msgError: MsgErrorType): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvDecryptionError") class RcvDecryptionError(val msgDecryptError: MsgDecryptError, val msgCount: UInt): CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvMsgError") class RcvMsgErrorContent(val rcvMsgError: RcvMsgError): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvGroupInvitation") class RcvGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndGroupInvitation") class SndGroupInvitation(val groupInvitation: CIGroupInvitation, val memberRole: GroupMemberRole): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvDirectEvent") class RcvDirectEventContent(val rcvDirectEvent: RcvDirectEvent): CIContent() { override val msgContent: MsgContent? get() = null } @@ -3770,6 +3772,7 @@ sealed class CIContent: ItemContent { is RcvCall -> status.text(duration) is RcvIntegrityError -> msgError.text is RcvDecryptionError -> msgDecryptError.text + is RcvMsgErrorContent -> rcvMsgError.text is RcvGroupInvitation -> groupInvitation.text is SndGroupInvitation -> groupInvitation.text is RcvDirectEventContent -> rcvDirectEvent.text @@ -3810,6 +3813,7 @@ sealed class CIContent: ItemContent { is RcvCall -> true is RcvIntegrityError -> true is RcvDecryptionError -> true + is RcvMsgErrorContent -> true is RcvGroupInvitation -> true is RcvModerated -> true is RcvBlocked -> true @@ -4721,6 +4725,17 @@ sealed class MsgErrorType() { } } +@Serializable +sealed class RcvMsgError() { + @Serializable @SerialName("dropped") class Dropped(val attempts: Int): RcvMsgError() + @Serializable @SerialName("parseError") class ParseError(val parseError: String): RcvMsgError() + + val text: String get() = when (this) { + is Dropped -> String.format(generalGetString(MR.strings.rcv_msg_error_dropped), attempts) + is ParseError -> String.format(generalGetString(MR.strings.rcv_msg_error_parse), parseError) + } +} + @Serializable sealed class RcvDirectEvent() { @Serializable @SerialName("contactDeleted") class ContactDeleted(): RcvDirectEvent() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index d2d0a91c9b..d5e2110063 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -714,6 +714,9 @@ fun ChatItemView( } else { Box(Modifier.size(0.dp)) {} } + is CIContent.RcvMsgErrorContent -> { + RcvMsgErrorItemView(c.rcvMsgError, cItem, showTimestamp, cInfo.timedMessagesTTL) + } is CIContent.RcvDecryptionError -> { CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) DeleteItemMenu() @@ -1310,6 +1313,7 @@ fun shapeStyleWithTail(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVis is CIContent.SndMsgContent, is CIContent.RcvMsgContent, is CIContent.RcvDecryptionError, + is CIContent.RcvMsgErrorContent, is CIContent.SndDeleted, is CIContent.RcvDeleted, is CIContent.RcvIntegrityError, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index d528396193..08b6520dfa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatItem import chat.simplex.common.model.MsgErrorType +import chat.simplex.common.model.RcvMsgError import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString @@ -73,6 +74,19 @@ fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onC } } +@Composable +fun RcvMsgErrorItemView(rcvMsgError: RcvMsgError, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) { + CIMsgError(ci, showTimestamp, timedMessagesTTL) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.alert_title_msg_error), + text = when (rcvMsgError) { + is RcvMsgError.Dropped -> String.format(generalGetString(MR.strings.alert_text_msg_reception_error), rcvMsgError.attempts) + is RcvMsgError.ParseError -> rcvMsgError.parseError + } + ) + } +} + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 609b6c4b3e..9ea32d5131 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1377,6 +1377,10 @@ bad message hash bad message ID duplicate message + dropped (%1$d attempts) + error: %s + Message error + The app removed this message after %1$d attempts to receive it. Skipped messages It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised. Bad message hash diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 7a68fa92a9..aee16ac5ea 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -70,6 +70,7 @@ This file is generated automatically. - [CreatedConnLink](#createdconnlink) - [CryptoFile](#cryptofile) - [CryptoFileArgs](#cryptofileargs) +- [DroppedMsg](#droppedmsg) - [E2EInfo](#e2einfo) - [ErrorType](#errortype) - [FeatureAllowed](#featureallowed) @@ -149,6 +150,7 @@ This file is generated automatically. - [RcvFileStatus](#rcvfilestatus) - [RcvFileTransfer](#rcvfiletransfer) - [RcvGroupEvent](#rcvgroupevent) +- [RcvMsgError](#rcvmsgerror) - [RelayProfile](#relayprofile) - [RelayStatus](#relaystatus) - [ReportReason](#reportreason) @@ -454,6 +456,10 @@ RcvDecryptionError: - msgDecryptError: [MsgDecryptError](#msgdecrypterror) - msgCount: word32 +RcvMsgError: +- type: "rcvMsgError" +- rcvMsgError: [RcvMsgError](#rcvmsgerror) + RcvGroupInvitation: - type: "rcvGroupInvitation" - groupInvitation: [CIGroupInvitation](#cigroupinvitation) @@ -1813,6 +1819,15 @@ connFullLink + ((' ' + connShortLink) if connShortLink is not None else '') # Py - fileNonce: string +--- + +## DroppedMsg + +**Record type**: +- brokerTs: UTCTime +- attempts: int + + --- ## E2EInfo @@ -3192,6 +3207,21 @@ MsgBadSignature: - type: "msgBadSignature" +--- + +## RcvMsgError + +**Discriminated union type**: + +Dropped: +- type: "dropped" +- attempts: int + +ParseError: +- type: "parseError" +- parseError: string + + --- ## RelayProfile @@ -3261,6 +3291,7 @@ A_CRYPTO: A_DUPLICATE: - type: "A_DUPLICATE" +- droppedMsg_: [DroppedMsg](#droppedmsg)? A_QUEUE: - type: "A_QUEUE" diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index e55e037243..826b8c1957 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -253,6 +253,7 @@ chatTypesDocsData = (sti @ContactUserPreferences, STRecord, "", [], "", ""), (sti @CryptoFile, STRecord, "", [], "", ""), (sti @CryptoFileArgs, STRecord, "", [], "", ""), + (sti @DroppedMsg, STRecord, "", [], "", ""), (sti @E2EInfo, STRecord, "", [], "", ""), (sti @ErrorType, STUnion, "", [], "", ""), (sti @FeatureAllowed, STEnum, "FA", [], "", ""), @@ -332,6 +333,7 @@ chatTypesDocsData = (sti @RcvFileStatus, STUnion, "RFS", [], "", ""), (sti @RcvFileTransfer, STRecord, "", [], "", ""), (sti @RcvGroupEvent, STUnion, "RGE", [], "", ""), + (sti @RcvMsgError, STUnion, "RME", [], "", ""), (sti @RelayProfile, STRecord, "", [], "", ""), (sti @RelayStatus, STEnum, "RS", [], "", ""), (sti @ReportReason, STEnum' (dropPfxSfx "RR" ""), "", ["RRUnknown"], "", ""), @@ -452,6 +454,7 @@ deriving instance Generic ContactStatus deriving instance Generic ContactUserPreferences deriving instance Generic CryptoFile deriving instance Generic CryptoFileArgs +deriving instance Generic DroppedMsg deriving instance Generic E2EInfo deriving instance Generic ErrorType deriving instance Generic FeatureAllowed @@ -537,6 +540,7 @@ deriving instance Generic RcvFileDescr deriving instance Generic RcvFileStatus deriving instance Generic RcvFileTransfer deriving instance Generic RcvGroupEvent +deriving instance Generic RcvMsgError deriving instance Generic RelayProfile deriving instance Generic RelayStatus deriving instance Generic ReportReason diff --git a/cabal.project b/cabal.project index e853187e42..43ea745379 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 97802a30fce1dfeea90f0b465e21fc8eca937abb + tag: 0933cbcb9ce67e055f5230a8d5e07f5cb41f887d source-repository-package type: git diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index ef232a6461..621a69dcc8 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -278,6 +278,7 @@ export type CIContent = | CIContent.RcvCall | CIContent.RcvIntegrityError | CIContent.RcvDecryptionError + | CIContent.RcvMsgError | CIContent.RcvGroupInvitation | CIContent.SndGroupInvitation | CIContent.RcvDirectEvent @@ -312,6 +313,7 @@ export namespace CIContent { | "rcvCall" | "rcvIntegrityError" | "rcvDecryptionError" + | "rcvMsgError" | "rcvGroupInvitation" | "sndGroupInvitation" | "rcvDirectEvent" @@ -383,6 +385,11 @@ export namespace CIContent { msgCount: number // word32 } + export interface RcvMsgError extends Interface { + type: "rcvMsgError" + rcvMsgError: RcvMsgError + } + export interface RcvGroupInvitation extends Interface { type: "rcvGroupInvitation" groupInvitation: CIGroupInvitation @@ -2075,6 +2082,11 @@ export interface CryptoFileArgs { fileNonce: string } +export interface DroppedMsg { + brokerTs: string // ISO-8601 timestamp + attempts: number // int +} + export interface E2EInfo { pqEnabled?: boolean } @@ -3606,6 +3618,26 @@ export namespace RcvGroupEvent { } } +export type RcvMsgError = RcvMsgError.Dropped | RcvMsgError.ParseError + +export namespace RcvMsgError { + export type Tag = "dropped" | "parseError" + + interface Interface { + type: Tag + } + + export interface Dropped extends Interface { + type: "dropped" + attempts: number // int + } + + export interface ParseError extends Interface { + type: "parseError" + parseError: string + } +} + export interface RelayProfile { displayName: string fullName: string @@ -3681,6 +3713,7 @@ export namespace SMPAgentError { export interface A_DUPLICATE extends Interface { type: "A_DUPLICATE" + droppedMsg_?: DroppedMsg } export interface A_QUEUE extends Interface { diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 1c660f4dfa..006f37104e 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."97802a30fce1dfeea90f0b465e21fc8eca937abb" = "1pgrfzir7www4f2h1byvzlihs6vird85m1mhwiinl0xaqsycha1m"; + "https://github.com/simplex-chat/simplexmq.git"."0933cbcb9ce67e055f5230a8d5e07f5cb41f887d" = "0mnqcfvv1hshknl5mv6b05sh6nylzsy98xmfgyczfnryqd88r2jc"; "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/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 2676224631..f5f7752ca4 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -493,7 +493,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e - eToView (ChatError . CEException $ "error parsing chat message: " <> e) + createInternalChatItem user (CDDirectRcv ct') (CIRcvMsgError $ RMEParseError $ T.pack e) Nothing + `catchAllErrors` \_ -> pure () withRcpt <- checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent pure (withRcpt, False) where @@ -687,6 +688,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- error cannot be AUTH error here updateDirectItemsStatusMsgs ct conn (L.toList msgIds) (CISSndError $ agentSndError err) eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + ERR (AGENT (A_DUPLICATE (Just DroppedMsg {brokerTs, attempts}))) -> + createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgError $ RMEDropped attempts) (Just brokerTs) ERR err -> do eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -962,7 +965,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "group msg=error " <> eInfo <> " " <> tshow e - eToView (ChatError . CEException $ "error parsing chat message: " <> e) + if isRelay membership + then + eToView (ChatError . CEException $ "error parsing chat message: " <> e) + else + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvMsgError $ RMEParseError $ T.pack e) Nothing + `catchAllErrors` \_ -> pure () pure newDeliveryTasks processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> VerifiedMsg e -> CM (Maybe NewMessageDeliveryTask) processEvent gInfo' m' verifiedMsg = do @@ -1178,6 +1186,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> forM_ msgIds $ \msgId -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure () eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + ERR err@(AGENT (A_DUPLICATE (Just DroppedMsg {brokerTs, attempts}))) + | isRelay membership -> + eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) + | otherwise -> do + (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo m') (CIRcvMsgError $ RMEDropped attempts) (Just brokerTs) ERR err -> do eToView $ ChatErrorAgent err (AgentConnId agentConnId) (Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index e2e878033d..b08cc5a991 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -145,6 +145,7 @@ data CIContent (d :: MsgDirection) where CIRcvCall :: CICallStatus -> Int -> CIContent 'MDRcv CIRcvIntegrityError :: MsgErrorType -> CIContent 'MDRcv CIRcvDecryptionError :: MsgDecryptError -> Word32 -> CIContent 'MDRcv + CIRcvMsgError :: RcvMsgError -> CIContent 'MDRcv CIRcvGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDRcv CISndGroupInvitation :: CIGroupInvitation -> GroupMemberRole -> CIContent 'MDSnd CIRcvDirectEvent :: RcvDirectEvent -> CIContent 'MDRcv @@ -196,6 +197,11 @@ data MsgDecryptError | MDERatchetSync deriving (Eq, Show) +data RcvMsgError + = RMEDropped {attempts :: Int} + | RMEParseError {parseError :: Text} + deriving (Eq, Show) + ciRequiresAttention :: forall d. MsgDirectionI d => CIContent d -> Bool ciRequiresAttention content = case msgDirection @d of SMDSnd -> True @@ -205,6 +211,7 @@ ciRequiresAttention content = case msgDirection @d of CIRcvCall {} -> True CIRcvIntegrityError _ -> True CIRcvDecryptionError {} -> True + CIRcvMsgError _ -> False CIRcvGroupInvitation {} -> True CIRcvDirectEvent rde -> case rde of RDEContactDeleted -> False @@ -275,6 +282,7 @@ ciContentToText = \case CIRcvCall status duration -> "incoming call: " <> ciCallInfoText status duration CIRcvIntegrityError err -> msgIntegrityError err CIRcvDecryptionError err n -> msgDecryptErrorText err n + CIRcvMsgError err -> rcvMsgErrorText err CIRcvGroupInvitation groupInvitation memberRole -> "received " <> ciGroupInvitationToText groupInvitation memberRole CISndGroupInvitation groupInvitation memberRole -> "sent " <> ciGroupInvitationToText groupInvitation memberRole CIRcvDirectEvent event -> rcvDirectEventToText event @@ -421,6 +429,11 @@ msgIntegrityError = \case MsgBadHash -> "incorrect message hash" MsgDuplicate -> "duplicate message ID" +rcvMsgErrorText :: RcvMsgError -> Text +rcvMsgErrorText = \case + RMEDropped {attempts} -> "message removed after " <> tshow attempts <> " attempts" + RMEParseError {parseError} -> "message error: " <> parseError + msgDecryptErrorText :: MsgDecryptError -> Word32 -> Text msgDecryptErrorText err n = "decryption error, possibly due to the device change" @@ -457,6 +470,7 @@ data JSONCIContent | JCIRcvCall {status :: CICallStatus, duration :: Int} | JCIRcvIntegrityError {msgError :: MsgErrorType} | JCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} + | JCIRcvMsgError {rcvMsgError :: RcvMsgError} | JCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | JCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | JCIRcvDirectEvent {rcvDirectEvent :: RcvDirectEvent} @@ -492,6 +506,7 @@ jsonCIContent = \case CIRcvCall status duration -> JCIRcvCall {status, duration} CIRcvIntegrityError err -> JCIRcvIntegrityError err CIRcvDecryptionError err n -> JCIRcvDecryptionError err n + CIRcvMsgError err -> JCIRcvMsgError err CIRcvGroupInvitation groupInvitation memberRole -> JCIRcvGroupInvitation {groupInvitation, memberRole} CISndGroupInvitation groupInvitation memberRole -> JCISndGroupInvitation {groupInvitation, memberRole} CIRcvDirectEvent rcvDirectEvent -> JCIRcvDirectEvent {rcvDirectEvent} @@ -527,6 +542,7 @@ aciContentJSON = \case JCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration JCIRcvIntegrityError err -> ACIContent SMDRcv $ CIRcvIntegrityError err JCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n + JCIRcvMsgError err -> ACIContent SMDRcv $ CIRcvMsgError err JCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole JCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole JCIRcvDirectEvent {rcvDirectEvent} -> ACIContent SMDRcv $ CIRcvDirectEvent rcvDirectEvent @@ -563,6 +579,7 @@ data DBJSONCIContent | DBJCIRcvCall {status :: CICallStatus, duration :: Int} | DBJCIRcvIntegrityError {msgError :: DBMsgErrorType} | DBJCIRcvDecryptionError {msgDecryptError :: MsgDecryptError, msgCount :: Word32} + | DBJCIRcvMsgError {rcvMsgError :: RcvMsgError} | DBJCIRcvGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | DBJCISndGroupInvitation {groupInvitation :: CIGroupInvitation, memberRole :: GroupMemberRole} | DBJCIRcvDirectEvent {rcvDirectEvent :: DBRcvDirectEvent} @@ -598,6 +615,7 @@ dbJsonCIContent = \case CIRcvCall status duration -> DBJCIRcvCall {status, duration} CIRcvIntegrityError err -> DBJCIRcvIntegrityError $ DBME err CIRcvDecryptionError err n -> DBJCIRcvDecryptionError err n + CIRcvMsgError err -> DBJCIRcvMsgError err CIRcvGroupInvitation groupInvitation memberRole -> DBJCIRcvGroupInvitation {groupInvitation, memberRole} CISndGroupInvitation groupInvitation memberRole -> DBJCISndGroupInvitation {groupInvitation, memberRole} CIRcvDirectEvent rde -> DBJCIRcvDirectEvent $ RDE rde @@ -633,6 +651,7 @@ aciContentDBJSON = \case DBJCIRcvCall {status, duration} -> ACIContent SMDRcv $ CIRcvCall status duration DBJCIRcvIntegrityError (DBME err) -> ACIContent SMDRcv $ CIRcvIntegrityError err DBJCIRcvDecryptionError err n -> ACIContent SMDRcv $ CIRcvDecryptionError err n + DBJCIRcvMsgError err -> ACIContent SMDRcv $ CIRcvMsgError err DBJCIRcvGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDRcv $ CIRcvGroupInvitation groupInvitation memberRole DBJCISndGroupInvitation {groupInvitation, memberRole} -> ACIContent SMDSnd $ CISndGroupInvitation groupInvitation memberRole DBJCIRcvDirectEvent (RDE rde) -> ACIContent SMDRcv $ CIRcvDirectEvent rde @@ -693,6 +712,8 @@ $(JQ.deriveJSON defaultJSON ''E2EInfo) $(JQ.deriveJSON (enumJSON $ dropPrefix "MDE") ''MsgDecryptError) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RME") ''RcvMsgError) + $(JQ.deriveJSON (enumJSON $ dropPrefix "CIGIS") ''CIGroupInvitationStatus) $(JQ.deriveJSON defaultJSON ''CIGroupInvitation) @@ -751,6 +772,7 @@ toCIContentTag ciContent = case ciContent of CIRcvCall {} -> "rcvCall" CIRcvIntegrityError _ -> "rcvIntegrityError" CIRcvDecryptionError {} -> "rcvDecryptionError" + CIRcvMsgError _ -> "rcvMsgError" CIRcvGroupInvitation {} -> "rcvGroupInvitation" CISndGroupInvitation {} -> "sndGroupInvitation" CIRcvDirectEvent _ -> "rcvDirectEvent" diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 6d7a094430..3485849741 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -56,7 +56,7 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) -import Simplex.Messaging.Compression (Compressed, compress1, decompress1) +import Simplex.Messaging.Compression (Compressed, compress1, decompress1, decompressedSize) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String @@ -799,7 +799,11 @@ parseChatMessages msg = case B.head msg of decodeCompressed :: ByteString -> [Either String AParsedMsg] decodeCompressed s = case smpDecode s of Left e -> [Left e] - Right (compressed :: L.NonEmpty Compressed) -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1 maxDecompressedMsgLength) compressed + Right (compressed :: L.NonEmpty Compressed) -> case traverse decompressedSize compressed of + Nothing -> [Left "compressed size not specified"] + Just sizes + | sum sizes > maxDecompressedMsgLength -> [Left "decompressed size exceeds limit"] + | otherwise -> concatMap (either (\e -> [Left e]) parseUncompressed' . decompress1) compressed parseUncompressed' "" = [Left "empty string"] parseUncompressed' s = parseUncompressed (B.head s) s -- Binary batch format: '=' ( )* diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 6959d3e562..3afe0ce0ce 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -671,6 +671,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa CIDirectRcv -> case content of CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvMsgError err -> viewRcvMsgError from err ts tz meta CIRcvGroupEvent {} -> showRcvItemProhibited from _ -> showRcvItem from where @@ -694,6 +695,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa rcvGroupItem m_ = case content of CIRcvMsgContent mc -> withRcvFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvMsgError err -> viewRcvMsgError from err ts tz meta CIRcvGroupInvitation {} | isJust m_ -> showRcvItemProhibited from CIRcvModerated {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False CIRcvBlocked {} -> receivedWithTime_ ts tz (ttyFromGroup g scopeInfo m_) context meta [plainContent content] False @@ -715,6 +717,7 @@ viewChatItem chat ci@ChatItem {chatDir, meta = meta@CIMeta {itemForwarded, forwa CILocalRcv -> case content of CIRcvMsgContent mc -> withLocalFile from $ rcvMsg from context mc CIRcvIntegrityError err -> viewRcvIntegrityError from err ts tz meta + CIRcvMsgError err -> viewRcvMsgError from err ts tz meta CIRcvGroupEvent {} -> showRcvItemProhibited from _ -> showRcvItem from where @@ -991,6 +994,9 @@ viewRcvIntegrityError from msgErr ts tz meta = receivedWithTime_ ts tz from [] m viewMsgIntegrityError :: MsgErrorType -> [StyledString] viewMsgIntegrityError err = [ttyError $ msgIntegrityError err] +viewRcvMsgError :: StyledString -> RcvMsgError -> CurrentTime -> TimeZone -> CIMeta c 'MDRcv -> [StyledString] +viewRcvMsgError from rcvErr ts tz meta = receivedWithTime_ ts tz from [] meta [ttyError $ rcvMsgErrorText rcvErr] False + viewInvalidConnReq :: [StyledString] viewInvalidConnReq = [ "", @@ -2656,7 +2662,7 @@ viewChatError isCmd logLevel testView = \case BRContent -> "content violates conditions of use" BROKER _ (NETWORK _) | not isCmd -> [] BROKER _ TIMEOUT | not isCmd -> [] - AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd] + AGENT A_DUPLICATE {} -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd] AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd] CONN NOT_FOUND _ -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd] CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]