diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 636ba99927..33d683288a 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1071,8 +1071,9 @@ public enum SMPHandshakeError: Decodable, Hashable { public enum SMPAgentError: Decodable, Hashable { case A_MESSAGE - case A_PROHIBITED + case A_PROHIBITED(prohibitedErr: String) case A_VERSION + case A_LINK(linkErr: String) case A_CRYPTO case A_DUPLICATE case A_QUEUE(queueErr: String) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index e6940b11d4..ddc9454b86 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -221,9 +221,36 @@ public struct LocalProfile: Codable, NamedChat, Hashable { ) } -public enum ChatPeerType: String, Codable { +public enum ChatPeerType: Hashable { case human case bot + case business + case unknown(String) + + public var text: String { + switch self { + case .human: "human" + case .bot: "bot" + case .business: "business" + case let .unknown(s): s + } + } +} + +extension ChatPeerType: Codable { + public init(from decoder: Decoder) throws { + switch try decoder.singleValueContainer().decode(String.self) { + case "human": self = .human + case "bot": self = .bot + case "business": self = .business + case let s: self = .unknown(s) + } + } + + public func encode(to encoder: Encoder) throws { + var c = encoder.singleValueContainer() + try c.encode(text) + } } // Supporter badge. The credential/proof bytes stay core-side; the UI only sees the disclosed type + status. 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 c9c30950ea..340ba2c1ad 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 @@ -2107,10 +2107,32 @@ data class LocalProfile( } } -@Serializable -enum class ChatPeerType { - @SerialName("human") Human, - @SerialName("bot") Bot +@Serializable(with = ChatPeerTypeSerializer::class) +sealed class ChatPeerType { + @Serializable @SerialName("human") object Human: ChatPeerType() + @Serializable @SerialName("bot") object Bot: ChatPeerType() + @Serializable @SerialName("business") object Business: ChatPeerType() + @Serializable @SerialName("unknown") data class Unknown(val type: String): ChatPeerType() + + val text: String + get() = when (this) { + is Human -> "human" + is Bot -> "bot" + is Business -> "business" + is Unknown -> type + } +} + +object ChatPeerTypeSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ChatPeerType", PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder): ChatPeerType = + when (val v = decoder.decodeString()) { + "human" -> ChatPeerType.Human + "bot" -> ChatPeerType.Bot + "business" -> ChatPeerType.Business + else -> ChatPeerType.Unknown(v) + } + override fun serialize(encoder: Encoder, value: ChatPeerType) = encoder.encodeString(value.text) } // Supporter badge. The credential/proof bytes stay core-side; the UI only sees the disclosed type + status. diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index f9438fca32..037c02b1c2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -8072,13 +8072,15 @@ sealed class SMPAgentError { is A_MESSAGE -> "A_MESSAGE" is A_PROHIBITED -> "A_PROHIBITED" is A_VERSION -> "A_VERSION" + is A_LINK -> "A_LINK" is A_CRYPTO -> "A_CRYPTO" is A_DUPLICATE -> "A_DUPLICATE" is A_QUEUE -> "A_QUEUE" } @Serializable @SerialName("A_MESSAGE") object A_MESSAGE: SMPAgentError() - @Serializable @SerialName("A_PROHIBITED") object A_PROHIBITED: SMPAgentError() + @Serializable @SerialName("A_PROHIBITED") class A_PROHIBITED(val prohibitedErr: String): SMPAgentError() @Serializable @SerialName("A_VERSION") object A_VERSION: SMPAgentError() + @Serializable @SerialName("A_LINK") class A_LINK(val linkErr: String): SMPAgentError() @Serializable @SerialName("A_CRYPTO") object A_CRYPTO: SMPAgentError() @Serializable @SerialName("A_DUPLICATE") object A_DUPLICATE: SMPAgentError() @Serializable @SerialName("A_QUEUE") class A_QUEUE(val queueErr: String): SMPAgentError() diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 893e687d78..1722bbfdd0 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -90,6 +90,7 @@ mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} = optFilesFolder = Nothing, optTempDirectory = Nothing, showReactions = False, + showFullLinks = False, allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index 2c94152a1c..199229964a 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -245,6 +245,7 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName, clientService} = optFilesFolder = Nothing, optTempDirectory = Nothing, showReactions = False, + showFullLinks = False, allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index 463dd4088b..8d61622fff 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -68,6 +68,9 @@ This file is generated automatically. - [APIUpdateProfile](#apiupdateprofile) - [APISetContactPrefs](#apisetcontactprefs) +[Service commands](#service-commands) +- [APISendServiceResponse](#apisendserviceresponse) + [Chat management](#chat-management) - [StartChat](#startchat) - [APIStopChat](#apistopchat) @@ -88,19 +91,20 @@ Create bot address. **Parameters**: - userId: int64 +- pqRatchet: bool? **Syntax**: ``` -/_address +/_address [ pq_ratchet=on|off] ``` ```javascript -'/_address ' + userId // JavaScript +'/_address ' + userId + (typeof pqRatchet == 'boolean' ? ' pq_ratchet=' + (pqRatchet ? 'on' : 'off') : '') // JavaScript ``` ```python -'/_address ' + str(userId) # Python +'/_address ' + str(userId) + ((' pq_ratchet=' + ('on' if pqRatchet else 'off')) if pqRatchet is not None else '') # Python ``` **Responses**: @@ -238,20 +242,21 @@ Set bot address settings. **Parameters**: - userId: int64 +- pqRatchet: bool? - settings: [AddressSettings](./TYPES.md#addresssettings) **Syntax**: ``` -/_address_settings +/_address_settings [ pq_ratchet=on|off] ``` ```javascript -'/_address_settings ' + userId + ' ' + JSON.stringify(settings) // JavaScript +'/_address_settings ' + userId + (typeof pqRatchet == 'boolean' ? ' pq_ratchet=' + (pqRatchet ? 'on' : 'off') : '') + ' ' + JSON.stringify(settings) // JavaScript ``` ```python -'/_address_settings ' + str(userId) + ' ' + json.dumps(settings) # Python +'/_address_settings ' + str(userId) + ((' pq_ratchet=' + ('on' if pqRatchet else 'off')) if pqRatchet is not None else '') + ' ' + json.dumps(settings) # Python ``` **Responses**: @@ -1551,6 +1556,7 @@ Reject contact request. The user who sent the request is **not notified**. **Parameters**: - contactReqId: int64 +- notify: bool **Syntax**: @@ -2118,6 +2124,50 @@ ChatCmdError: Command error (only used in WebSockets API). --- +## Service commands + +Bots with a double ratchet address can answer service requests. + + +### APISendServiceResponse + +Send a reply to a received service request. Returns the connection ID that correlates the reply delivery event. + +*Network usage*: background. + +**Parameters**: +- userId: int64 +- requestId: string +- responseData: JSONObject + +**Syntax**: + +``` +/_service_response +``` + +```javascript +'/_service_response ' + userId + ' ' + requestId + ' ' + JSON.stringify(responseData) // JavaScript +``` + +```python +'/_service_response ' + str(userId) + ' ' + requestId + ' ' + json.dumps(responseData) # Python +``` + +**Responses**: + +ServiceReplyAccepted: Service reply accepted for delivery. `connectionId` correlates the reply delivery event.. +- type: "serviceReplyAccepted" +- user: [User](./TYPES.md#user) +- connectionId: string + +ChatCmdError: Command error (only used in WebSockets API). +- type: "chatCmdError" +- chatError: [ChatError](./TYPES.md#chaterror) + +--- + + ## Chat management These commands should not be used with CLI-based bots @@ -2132,11 +2182,20 @@ Start chat controller. **Parameters**: - mainApp: bool - enableSndFiles: bool +- serviceRequests: bool **Syntax**: ``` -/_start +/_start main=on|off[ snd_files=off][ service_requests=on] +``` + +```javascript +'/_start main=' + (mainApp ? 'on' : 'off') + (!enableSndFiles ? ' snd_files=off' : '') + (serviceRequests ? ' service_requests=on' : '') // JavaScript +``` + +```python +'/_start' + ' main=' + ('on' if mainApp else 'off') + (' snd_files=off' if not enableSndFiles else '') + (' service_requests=on' if serviceRequests else '') # Python ``` **Responses**: diff --git a/bots/api/EVENTS.md b/bots/api/EVENTS.md index 947c60586a..d2744951cd 100644 --- a/bots/api/EVENTS.md +++ b/bots/api/EVENTS.md @@ -61,7 +61,6 @@ This file is generated automatically. - [ContactConnecting](#contactconnecting) - [BusinessLinkConnecting](#businesslinkconnecting) - [JoinedGroupMemberConnecting](#joinedgroupmemberconnecting) -- [SentGroupInvitation](#sentgroupinvitation) - [GroupLinkConnecting](#grouplinkconnecting) [Network connection events](#network-connection-events) @@ -69,6 +68,10 @@ This file is generated automatically. - [HostDisconnected](#hostdisconnected) - [SubscriptionStatus](#subscriptionstatus) +[Service events](#service-events) +- [ServiceRequest](#servicerequest) +- [ServiceReplySent](#servicereplysent) + [Error events](#error-events) - [MessageError](#messageerror) - [ChatError](#chaterror) @@ -701,20 +704,6 @@ Group member is announced to the group and will be connecting to bot. --- -### SentGroupInvitation - -Sent when another user joins group via bot's link. - -**Record type**: -- type: "sentGroupInvitation" -- user: [User](./TYPES.md#user) -- groupInfo: [GroupInfo](./TYPES.md#groupinfo) -- contact: [Contact](./TYPES.md#contact) -- member: [GroupMember](./TYPES.md#groupmember) - ---- - - ### GroupLinkConnecting Sent when bot joins group via another user link. @@ -770,6 +759,40 @@ Messaging subscription status changed --- +## Service events + +Bots with a double ratchet address, started with service request processing enabled, can answer service requests - a single request with a single response (RPC). + + +### ServiceRequest + +Service request received. + +The request needs to be answered using [APISendServiceResponse](./COMMANDS.md#apisendserviceresponse) command. + +**Record type**: +- type: "serviceRequest" +- user: [User](./TYPES.md#user) +- requestId: string +- signerKey: string? +- requestData: JSONObject + +--- + + +### ServiceReplySent + +Service reply was sent (delivered to the server). + +Correlate `connectionId` with the connection ID from the response to [APISendServiceResponse](./COMMANDS.md#apisendserviceresponse) to learn when the reply is delivered. + +**Record type**: +- type: "serviceReplySent" +- connectionId: string + +--- + + ## Error events Bots may log these events for debugging. There will be many error events - this does NOT indicate a malfunction - e.g., they may happen because of bad network connectivity, or because messages may be delivered to deleted chats for a short period of time (they will be ignored). diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 79338930c8..61c8c97132 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -9,6 +9,7 @@ This file is generated automatically. - [AddressSettings](#addresssettings) - [AgentCryptoError](#agentcryptoerror) - [AgentErrorType](#agenterrortype) +- [AgentServiceError](#agentserviceerror) - [AutoAccept](#autoaccept) - [BadgeInfo](#badgeinfo) - [BadgeProof](#badgeproof) @@ -206,6 +207,7 @@ This file is generated automatically. - [UserContact](#usercontact) - [UserContactLink](#usercontactlink) - [UserContactRequest](#usercontactrequest) +- [UserContactRequestRef](#usercontactrequestref) - [UserInfo](#userinfo) - [UserProfileUpdateSummary](#userprofileupdatesummary) - [UserPwdHash](#userpwdhash) @@ -360,6 +362,29 @@ INACTIVE: - type: "INACTIVE" +--- + +## AgentServiceError + +**Discriminated union type**: + +Rejected: +- type: "rejected" +- rejectReason: string + +Timeout: +- type: "timeout" + +NoPendingRequest: +- type: "noPendingRequest" + +NotDRAddress: +- type: "notDRAddress" + +BadSignature: +- type: "badSignature" + + --- ## AutoAccept @@ -1782,6 +1807,7 @@ Error: - chatTs: UTCTime? - preparedContact: [PreparedContact](#preparedcontact)? - contactRequestId: int64? +- contactRequest: [UserContactRequestRef](#usercontactrequestref)? - contactGroupMemberId: int64? - contactGrpInvSent: bool - groupDirectInv: [GroupDirectInvitation](#groupdirectinvitation)? @@ -1841,6 +1867,7 @@ ContactViaAddress: - "active" - "deleted" - "deletedByUser" +- "rejected" --- @@ -3585,6 +3612,10 @@ A_QUEUE: - type: "A_QUEUE" - queueErr: string +A_SERVICE: +- type: "A_SERVICE" +- serviceError: [AgentServiceError](#agentserviceerror) + --- @@ -4397,6 +4428,16 @@ Handshake: - pqSupport: bool - welcomeSharedMsgId: string? - requestSharedMsgId: string? +- rejectionSupported: bool + + +--- + +## UserContactRequestRef + +**Record type**: +- contactRequestId: int64 +- rejectionSupported: bool --- diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 46f8b6032d..da2a9e5d59 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -77,11 +77,11 @@ chatCommandsDocsData :: [(String, String, [(ConsName, [String], Text, [ConsName] chatCommandsDocsData = [ ( "Address commands", "Bots can use these commands to automatically check and create address when initialized", - [ ("APICreateMyAddress", ["server_"], "Create bot address.", ["CRUserContactLinkCreated", "CRChatCmdError"], [], Just UNInteractive, "/_address " <> Param "userId"), + [ ("APICreateMyAddress", ["server_"], "Create bot address.", ["CRUserContactLinkCreated", "CRChatCmdError"], [], Just UNInteractive, "/_address " <> Param "userId" <> OnOffParam "pq_ratchet" "pqRatchet" Nothing), ("APIDeleteMyAddress", [], "Delete bot address.", ["CRUserContactLinkDeleted", "CRChatCmdError"], [], Just UNBackground, "/_delete_address " <> Param "userId"), ("APIShowMyAddress", [], "Get bot address and settings.", ["CRUserContactLink", "CRChatCmdError"], [], Nothing, "/_show_address " <> Param "userId"), ("APISetProfileAddress", [], "Add address to bot profile.", ["CRUserProfileUpdated", "CRChatCmdError"], [], Just UNInteractive, "/_profile_address " <> Param "userId" <> " " <> OnOff "enable"), - ("APISetAddressSettings", [], "Set bot address settings.", ["CRUserContactLinkUpdated", "CRChatCmdError"], [], Just UNInteractive, "/_address_settings " <> Param "userId" <> " " <> Json "settings") + ("APISetAddressSettings", [], "Set bot address settings.", ["CRUserContactLinkUpdated", "CRChatCmdError"], [], Just UNInteractive, "/_address_settings " <> Param "userId" <> OnOffParam "pq_ratchet" "pqRatchet" Nothing <> " " <> Json "settings") ] ), ( "Message commands", @@ -188,9 +188,14 @@ chatCommandsDocsData = ("APISetContactPrefs", [], "Configure chat preference overrides for the contact.", ["CRContactPrefsUpdated", "CRChatCmdError"], [], Just UNBackground, "/_set prefs @" <> Param "contactId" <> " " <> Json "preferences") ] ), + ( "Service commands", + "Bots with a double ratchet address can answer service requests.", + [ ("APISendServiceResponse", [], "Send a reply to a received service request. Returns the connection ID that correlates the reply delivery event.", ["CRServiceReplyAccepted", "CRChatCmdError"], [], Just UNBackground, "/_service_response " <> Param "userId" <> " " <> Param "requestId" <> " " <> Json "responseData") + ] + ), ( "Chat management", "These commands should not be used with CLI-based bots", - [ ("StartChat", [], "Start chat controller.", ["CRChatStarted", "CRChatRunning"], [], Nothing, "/_start"), + [ ("StartChat", [], "Start chat controller.", ["CRChatStarted", "CRChatRunning"], [], Nothing, "/_start" <> OnOffParam "main" "mainApp" Nothing <> OnOffParam "snd_files" "enableSndFiles" (Just True) <> OnOffParam "service_requests" "serviceRequests" (Just False)), ("APIStopChat", [], "Stop chat controller.", ["CRChatStopped"], [], Nothing, "/_stop") ] ) @@ -392,11 +397,13 @@ undocumentedCommands = "APIRejectCall", "APIReorderChatTags", "APIReportMessage", + "APIRotateAddressRatchetKeys", "APISaveAppSettings", "APISendCallAnswer", "APISendCallExtraInfo", "APISendCallInvitation", "APISendCallOffer", + "APISendServiceRequest", "APISetAppFilePaths", "APISetChatItemTTL", "APISetChatSettings", diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index f0c9352efd..1590ad54d5 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -133,7 +133,6 @@ chatEventsDocsData = ("CEvtContactConnecting", "Contact confirmed connection.\n\nSent when contact started connecting via bot's 1-time invitation link or when bot connects to another SimpleX address."), -- CONF ("CEvtBusinessLinkConnecting", "Contact confirmed connection.\n\nSent when bot connects to another business address."), -- CONF ("CEvtJoinedGroupMemberConnecting", "Group member is announced to the group and will be connecting to bot."), -- MSG - ("CEvtSentGroupInvitation", "Sent when another user joins group via bot's link."), -- INV ("CEvtGroupLinkConnecting", "Sent when bot joins group via another user link.") -- CONF ], [] @@ -146,6 +145,13 @@ chatEventsDocsData = ], [] ), + ( "Service events", + "Bots with a double ratchet address, started with service request processing enabled, can answer service requests - a single request with a single response (RPC).", + [ ("CEvtServiceRequest", "Service request received.\n\nThe request needs to be answered using [APISendServiceResponse](./COMMANDS.md#apisendserviceresponse) command."), + ("CEvtServiceReplySent", "Service reply was sent (delivered to the server).\n\nCorrelate `connectionId` with the connection ID from the response to [APISendServiceResponse](./COMMANDS.md#apisendserviceresponse) to learn when the reply is delivered.") + ], + [] + ), ( "Error events", "Bots may log these events for debugging. \ \There will be many error events - this does NOT indicate a malfunction - \ @@ -184,6 +190,7 @@ undocumentedEvents = "CEvtContactPQEnabled", "CEvtContactRatchetSync", "CEvtContactRequestAlreadyAccepted", + "CEvtContactRequestRejected", "CEvtContactSwitch", "CEvtCustomChatEvent", "CEvtGroupMemberRatchetSync", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index f897fc3908..7f158f7540 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -89,6 +89,7 @@ chatResponsesDocsData = ("CRSentConfirmation", "Confirmation sent to one-time invitation"), ("CRSentGroupInvitation", "Group invitation sent"), ("CRSentInvitation", "Invitation sent to contact address"), + ("CRServiceReplyAccepted", "Service reply accepted for delivery. `connectionId` correlates the reply delivery event."), ("CRSndFileCancelled", "Cancelled sending file"), ("CRUserAcceptedGroupSent", "User accepted group invitation"), ("CRUserContactLink", "User contact address"), @@ -196,6 +197,7 @@ undocumentedResponses = "CRSentInvitationToContact", "CRServerOperatorConditions", "CRServerTestResult", + "CRServiceResponse", "CRSlowSQLQueries", "CRSndStandaloneFileCreated", "CRSQLResult", diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 5e1e2bb082..f6b25934b1 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -198,6 +198,7 @@ chatTypesDocsData = -- (STI "JSONObject" [], STRecord, "", [], "Arbitrary JSON object."), -- (STI "UTCTime" [], STRecord, "", [], "Timestampe in ISO8601 format as string."), (STI "VersionRange" [RecordTypeInfo "VersionRange" [FieldInfo "minVersion" (ti TInt), FieldInfo "maxVersion" (ti TInt)]], STRecord, "", [], "", ""), + (STI "UserContactRequestRef" [RecordTypeInfo "UserContactRequestRef" [FieldInfo "contactRequestId" (ti TInt64), FieldInfo "rejectionSupported" (ti TBool)]], STRecord, "", [], "", ""), (sti @(ChatItem 'CTDirect 'MDSnd), STRecord, "", [], "", ""), (sti @(CIFile 'MDSnd), STRecord, "", [], "", ""), (sti @(CIMeta 'CTDirect 'MDSnd), STRecord, "", [], "", ""), @@ -209,6 +210,7 @@ chatTypesDocsData = (sti @AddressSettings, STRecord, "", [], "", ""), (sti @AgentCryptoError, STUnion, "", ["RATCHET_EARLIER", "RATCHET_SKIPPED"], "", ""), -- TODO add fields to types (sti @AgentErrorType, STUnion, "", [], "", ""), + (sti @AgentServiceError, STUnion, "ASE", [], "", ""), (sti @AutoAccept, STRecord, "", [], "", ""), (sti @BadgeProof, STRecord, "", [], "", ""), (sti @BlockingInfo, STRecord, "", [], "", ""), @@ -435,6 +437,7 @@ deriving instance Generic AddRelayResult deriving instance Generic AddressSettings deriving instance Generic AgentCryptoError deriving instance Generic AgentErrorType +deriving instance Generic AgentServiceError deriving instance Generic AutoAccept deriving instance Generic BadgeProof deriving instance Generic BlockingInfo diff --git a/cabal.project b/cabal.project index 3c0526fa6e..dbb26ad0f2 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: efaad8e73436d60f5052f07dda6b71151ad5039b + tag: ee4dd0d8ded0f66f70a8890ce09d69e3610aa276 source-repository-package type: git diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index 3f4e3ad7ee..4c7c13403e 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -12,13 +12,14 @@ import {CR} from "./responses" // Network usage: interactive. export interface APICreateMyAddress { userId: number // int64 + pqRatchet?: boolean } export namespace APICreateMyAddress { export type Response = CR.UserContactLinkCreated | CR.ChatCmdError export function cmdString(self: APICreateMyAddress): string { - return '/_address ' + self.userId + return '/_address ' + self.userId + (typeof self.pqRatchet == 'boolean' ? ' pq_ratchet=' + (self.pqRatchet ? 'on' : 'off') : '') } } @@ -69,6 +70,7 @@ export namespace APISetProfileAddress { // Network usage: interactive. export interface APISetAddressSettings { userId: number // int64 + pqRatchet?: boolean settings: T.AddressSettings } @@ -76,7 +78,7 @@ export namespace APISetAddressSettings { export type Response = CR.UserContactLinkUpdated | CR.ChatCmdError export function cmdString(self: APISetAddressSettings): string { - return '/_address_settings ' + self.userId + ' ' + JSON.stringify(self.settings) + return '/_address_settings ' + self.userId + (typeof self.pqRatchet == 'boolean' ? ' pq_ratchet=' + (self.pqRatchet ? 'on' : 'off') : '') + ' ' + JSON.stringify(self.settings) } } @@ -562,6 +564,7 @@ export namespace APIAcceptContact { // Network usage: no. export interface APIRejectContact { contactReqId: number // int64 + notify: boolean } export namespace APIRejectContact { @@ -786,6 +789,25 @@ export namespace APISetContactPrefs { } } +// Service commands +// Bots with a double ratchet address can answer service requests. + +// Send a reply to a received service request. Returns the connection ID that correlates the reply delivery event. +// Network usage: background. +export interface APISendServiceResponse { + userId: number // int64 + requestId: string + responseData: object +} + +export namespace APISendServiceResponse { + export type Response = CR.ServiceReplyAccepted | CR.ChatCmdError + + export function cmdString(self: APISendServiceResponse): string { + return '/_service_response ' + self.userId + ' ' + self.requestId + ' ' + JSON.stringify(self.responseData) + } +} + // Chat management // These commands should not be used with CLI-based bots @@ -794,13 +816,14 @@ export namespace APISetContactPrefs { export interface StartChat { mainApp: boolean enableSndFiles: boolean + serviceRequests: boolean } export namespace StartChat { export type Response = CR.ChatStarted | CR.ChatRunning - export function cmdString(_self: StartChat): string { - return '/_start' + export function cmdString(self: StartChat): string { + return '/_start main=' + (self.mainApp ? 'on' : 'off') + (!self.enableSndFiles ? ' snd_files=off' : '') + (self.serviceRequests ? ' service_requests=on' : '') } } diff --git a/packages/simplex-chat-client/types/typescript/src/events.ts b/packages/simplex-chat-client/types/typescript/src/events.ts index cc19305913..59e0c3274d 100644 --- a/packages/simplex-chat-client/types/typescript/src/events.ts +++ b/packages/simplex-chat-client/types/typescript/src/events.ts @@ -46,11 +46,12 @@ export type ChatEvent = | CEvt.ContactConnecting | CEvt.BusinessLinkConnecting | CEvt.JoinedGroupMemberConnecting - | CEvt.SentGroupInvitation | CEvt.GroupLinkConnecting | CEvt.HostConnected | CEvt.HostDisconnected | CEvt.SubscriptionStatus + | CEvt.ServiceRequest + | CEvt.ServiceReplySent | CEvt.MessageError | CEvt.ChatError | CEvt.ChatErrors @@ -99,11 +100,12 @@ export namespace CEvt { | "contactConnecting" | "businessLinkConnecting" | "joinedGroupMemberConnecting" - | "sentGroupInvitation" | "groupLinkConnecting" | "hostConnected" | "hostDisconnected" | "subscriptionStatus" + | "serviceRequest" + | "serviceReplySent" | "messageError" | "chatError" | "chatErrors" @@ -430,14 +432,6 @@ export namespace CEvt { member: T.GroupMember } - export interface SentGroupInvitation extends Interface { - type: "sentGroupInvitation" - user: T.User - groupInfo: T.GroupInfo - contact: T.Contact - member: T.GroupMember - } - export interface GroupLinkConnecting extends Interface { type: "groupLinkConnecting" user: T.User @@ -464,6 +458,19 @@ export namespace CEvt { connections: string[] } + export interface ServiceRequest extends Interface { + type: "serviceRequest" + user: T.User + requestId: string + signerKey?: string + requestData: object + } + + export interface ServiceReplySent extends Interface { + type: "serviceReplySent" + connectionId: string + } + export interface MessageError extends Interface { type: "messageError" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index f54acfbeb1..fa5c83d03c 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -48,6 +48,7 @@ export type ChatResponse = | CR.SentConfirmation | CR.SentGroupInvitation | CR.SentInvitation + | CR.ServiceReplyAccepted | CR.SndFileCancelled | CR.UserAcceptedGroupSent | CR.UserContactLink @@ -106,6 +107,7 @@ export namespace CR { | "sentConfirmation" | "sentGroupInvitation" | "sentInvitation" + | "serviceReplyAccepted" | "sndFileCancelled" | "userAcceptedGroupSent" | "userContactLink" @@ -409,6 +411,12 @@ export namespace CR { customUserProfile?: T.Profile } + export interface ServiceReplyAccepted extends Interface { + type: "serviceReplyAccepted" + user: T.User + connectionId: string + } + export interface SndFileCancelled extends Interface { type: "sndFileCancelled" user: T.User diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index e2e30d43bc..abc4187747 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -188,6 +188,42 @@ export namespace AgentErrorType { } } +export type AgentServiceError = + | AgentServiceError.Rejected + | AgentServiceError.Timeout + | AgentServiceError.NoPendingRequest + | AgentServiceError.NotDRAddress + | AgentServiceError.BadSignature + +export namespace AgentServiceError { + export type Tag = "rejected" | "timeout" | "noPendingRequest" | "notDRAddress" | "badSignature" + + interface Interface { + type: Tag + } + + export interface Rejected extends Interface { + type: "rejected" + rejectReason: string + } + + export interface Timeout extends Interface { + type: "timeout" + } + + export interface NoPendingRequest extends Interface { + type: "noPendingRequest" + } + + export interface NotDRAddress extends Interface { + type: "notDRAddress" + } + + export interface BadSignature extends Interface { + type: "badSignature" + } +} + export interface AutoAccept { acceptIncognito: boolean } @@ -2021,6 +2057,7 @@ export interface Contact { chatTs?: string // ISO-8601 timestamp preparedContact?: PreparedContact contactRequestId?: number // int64 + contactRequest?: UserContactRequestRef contactGroupMemberId?: number // int64 contactGrpInvSent: boolean groupDirectInv?: GroupDirectInvitation @@ -2093,6 +2130,7 @@ export enum ContactStatus { Active = "active", Deleted = "deleted", DeletedByUser = "deletedByUser", + Rejected = "rejected", } export type ContactUserPref = ContactUserPref.Contact | ContactUserPref.User @@ -3926,6 +3964,7 @@ export type SMPAgentError = | SMPAgentError.A_CRYPTO | SMPAgentError.A_DUPLICATE | SMPAgentError.A_QUEUE + | SMPAgentError.A_SERVICE export namespace SMPAgentError { export type Tag = @@ -3936,6 +3975,7 @@ export namespace SMPAgentError { | "A_CRYPTO" | "A_DUPLICATE" | "A_QUEUE" + | "A_SERVICE" interface Interface { type: Tag @@ -3973,6 +4013,11 @@ export namespace SMPAgentError { type: "A_QUEUE" queueErr: string } + + export interface A_SERVICE extends Interface { + type: "A_SERVICE" + serviceError: AgentServiceError + } } export interface SecurityCode { @@ -5043,6 +5088,12 @@ export interface UserContactRequest { pqSupport: boolean welcomeSharedMsgId?: string requestSharedMsgId?: string + rejectionSupported: boolean +} + +export interface UserContactRequestRef { + contactRequestId: number // int64 + rejectionSupported: boolean } export interface UserInfo { diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index a7f4a56465..1886df7868 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -13,10 +13,11 @@ from . import _responses as CR # Network usage: interactive. class APICreateMyAddress(TypedDict): userId: int # int64 + pqRatchet: NotRequired[bool] def APICreateMyAddress_cmd_string(self: APICreateMyAddress) -> str: - return '/_address ' + str(self['userId']) + return '/_address ' + str(self['userId']) + ((' pq_ratchet=' + ('on' if self.get('pqRatchet') else 'off')) if self.get('pqRatchet') is not None else '') APICreateMyAddress_Response = CR.UserContactLinkCreated | CR.ChatCmdError @@ -62,11 +63,12 @@ APISetProfileAddress_Response = CR.UserProfileUpdated | CR.ChatCmdError # Network usage: interactive. class APISetAddressSettings(TypedDict): userId: int # int64 + pqRatchet: NotRequired[bool] settings: "T.AddressSettings" def APISetAddressSettings_cmd_string(self: APISetAddressSettings) -> str: - return '/_address_settings ' + str(self['userId']) + ' ' + json.dumps(self['settings']) + return '/_address_settings ' + str(self['userId']) + ((' pq_ratchet=' + ('on' if self.get('pqRatchet') else 'off')) if self.get('pqRatchet') is not None else '') + ' ' + json.dumps(self['settings']) APISetAddressSettings_Response = CR.UserContactLinkUpdated | CR.ChatCmdError @@ -493,6 +495,7 @@ APIAcceptContact_Response = CR.AcceptingContactRequest | CR.ChatCmdError # Network usage: no. class APIRejectContact(TypedDict): contactReqId: int # int64 + notify: bool def APIRejectContact_cmd_string(self: APIRejectContact) -> str: @@ -689,6 +692,23 @@ def APISetContactPrefs_cmd_string(self: APISetContactPrefs) -> str: APISetContactPrefs_Response = CR.ContactPrefsUpdated | CR.ChatCmdError +# Service commands +# Bots with a double ratchet address can answer service requests. + +# Send a reply to a received service request. Returns the connection ID that correlates the reply delivery event. +# Network usage: background. +class APISendServiceResponse(TypedDict): + userId: int # int64 + requestId: str + responseData: dict[str, object] + + +def APISendServiceResponse_cmd_string(self: APISendServiceResponse) -> str: + return '/_service_response ' + str(self['userId']) + ' ' + self['requestId'] + ' ' + json.dumps(self['responseData']) + +APISendServiceResponse_Response = CR.ServiceReplyAccepted | CR.ChatCmdError + + # Chat management # These commands should not be used with CLI-based bots @@ -697,10 +717,11 @@ APISetContactPrefs_Response = CR.ContactPrefsUpdated | CR.ChatCmdError class StartChat(TypedDict): mainApp: bool enableSndFiles: bool + serviceRequests: bool def StartChat_cmd_string(self: StartChat) -> str: - return '/_start' + return '/_start' + ' main=' + ('on' if self['mainApp'] else 'off') + (' snd_files=off' if not self['enableSndFiles'] else '') + (' service_requests=on' if self['serviceRequests'] else '') StartChat_Response = CR.ChatStarted | CR.ChatRunning diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_events.py b/packages/simplex-chat-python/src/simplex_chat/types/_events.py index 7b7c724c92..58ead11837 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_events.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_events.py @@ -281,13 +281,6 @@ class JoinedGroupMemberConnecting(TypedDict): hostMember: "T.GroupMember" member: "T.GroupMember" -class SentGroupInvitation(TypedDict): - type: Literal["sentGroupInvitation"] - user: "T.User" - groupInfo: "T.GroupInfo" - contact: "T.Contact" - member: "T.GroupMember" - class GroupLinkConnecting(TypedDict): type: Literal["groupLinkConnecting"] user: "T.User" @@ -310,6 +303,17 @@ class SubscriptionStatus(TypedDict): subscriptionStatus: "T.SubscriptionStatus" connections: list[str] +class ServiceRequest(TypedDict): + type: Literal["serviceRequest"] + user: "T.User" + requestId: str + signerKey: NotRequired[str] + requestData: dict[str, object] + +class ServiceReplySent(TypedDict): + type: Literal["serviceReplySent"] + connectionId: str + class MessageError(TypedDict): type: Literal["messageError"] user: "T.User" @@ -367,17 +371,18 @@ ChatEvent = ( | ContactConnecting | BusinessLinkConnecting | JoinedGroupMemberConnecting - | SentGroupInvitation | GroupLinkConnecting | HostConnected | HostDisconnected | SubscriptionStatus + | ServiceRequest + | ServiceReplySent | MessageError | ChatError | ChatErrors ) -ChatEvent_Tag = Literal["contactConnected", "contactUpdated", "contactDeletedByContact", "receivedContactRequest", "newMemberContactReceivedInv", "contactSndReady", "newChatItems", "chatItemReaction", "chatItemsDeleted", "chatItemUpdated", "groupChatItemsDeleted", "chatItemsStatusesUpdated", "receivedGroupInvitation", "userJoinedGroup", "groupUpdated", "joinedGroupMember", "memberRole", "deletedMember", "leftMember", "deletedMemberUser", "groupDeleted", "connectedToGroupMember", "memberAcceptedByOther", "memberBlockedForAll", "groupMemberUpdated", "groupLinkDataUpdated", "groupRelayUpdated", "rcvFileDescrReady", "rcvFileComplete", "sndFileCompleteXFTP", "rcvFileStart", "rcvFileSndCancelled", "rcvFileAccepted", "rcvFileError", "rcvFileWarning", "sndFileError", "sndFileWarning", "acceptingContactRequest", "acceptingBusinessRequest", "contactConnecting", "businessLinkConnecting", "joinedGroupMemberConnecting", "sentGroupInvitation", "groupLinkConnecting", "hostConnected", "hostDisconnected", "subscriptionStatus", "messageError", "chatError", "chatErrors"] +ChatEvent_Tag = Literal["contactConnected", "contactUpdated", "contactDeletedByContact", "receivedContactRequest", "newMemberContactReceivedInv", "contactSndReady", "newChatItems", "chatItemReaction", "chatItemsDeleted", "chatItemUpdated", "groupChatItemsDeleted", "chatItemsStatusesUpdated", "receivedGroupInvitation", "userJoinedGroup", "groupUpdated", "joinedGroupMember", "memberRole", "deletedMember", "leftMember", "deletedMemberUser", "groupDeleted", "connectedToGroupMember", "memberAcceptedByOther", "memberBlockedForAll", "groupMemberUpdated", "groupLinkDataUpdated", "groupRelayUpdated", "rcvFileDescrReady", "rcvFileComplete", "sndFileCompleteXFTP", "rcvFileStart", "rcvFileSndCancelled", "rcvFileAccepted", "rcvFileError", "rcvFileWarning", "sndFileError", "sndFileWarning", "acceptingContactRequest", "acceptingBusinessRequest", "contactConnecting", "businessLinkConnecting", "joinedGroupMemberConnecting", "groupLinkConnecting", "hostConnected", "hostDisconnected", "subscriptionStatus", "serviceRequest", "serviceReplySent", "messageError", "chatError", "chatErrors"] class OnEventDecorator(Protocol): @@ -640,12 +645,6 @@ class OnEventDecorator(Protocol): Callable[["JoinedGroupMemberConnecting"], Awaitable[None]], ]: ... - @overload - def __call__(self, event: Literal["sentGroupInvitation"], /) -> Callable[ - [Callable[["SentGroupInvitation"], Awaitable[None]]], - Callable[["SentGroupInvitation"], Awaitable[None]], - ]: ... - @overload def __call__(self, event: Literal["groupLinkConnecting"], /) -> Callable[ [Callable[["GroupLinkConnecting"], Awaitable[None]]], @@ -670,6 +669,18 @@ class OnEventDecorator(Protocol): Callable[["SubscriptionStatus"], Awaitable[None]], ]: ... + @overload + def __call__(self, event: Literal["serviceRequest"], /) -> Callable[ + [Callable[["ServiceRequest"], Awaitable[None]]], + Callable[["ServiceRequest"], Awaitable[None]], + ]: ... + + @overload + def __call__(self, event: Literal["serviceReplySent"], /) -> Callable[ + [Callable[["ServiceReplySent"], Awaitable[None]]], + Callable[["ServiceReplySent"], Awaitable[None]], + ]: ... + @overload def __call__(self, event: Literal["messageError"], /) -> Callable[ [Callable[["MessageError"], Awaitable[None]]], diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py index bd61e6dae1..955291d0f0 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py @@ -247,6 +247,11 @@ class SentInvitation(TypedDict): connection: "T.PendingContactConnection" customUserProfile: NotRequired["T.Profile"] +class ServiceReplyAccepted(TypedDict): + type: Literal["serviceReplyAccepted"] + user: "T.User" + connectionId: str + class SndFileCancelled(TypedDict): type: Literal["sndFileCancelled"] user: "T.User" @@ -352,6 +357,7 @@ ChatResponse = ( | SentConfirmation | SentGroupInvitation | SentInvitation + | ServiceReplyAccepted | SndFileCancelled | UserAcceptedGroupSent | UserContactLink @@ -365,4 +371,4 @@ ChatResponse = ( | ApiChats ) -ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "relayGroupAllowed", "groupMembers", "groupUpdated", "groupsList", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "sentConfirmation", "sentGroupInvitation", "sentInvitation", "sndFileCancelled", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"] +ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "relayGroupAllowed", "groupMembers", "groupUpdated", "groupsList", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "sentConfirmation", "sentGroupInvitation", "sentInvitation", "serviceReplyAccepted", "sndFileCancelled", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"] diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 96d3f4dea4..b57915d2db 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -139,6 +139,32 @@ AgentErrorType = ( AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "NO_NAME_SERVERS", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"] +class AgentServiceError_rejected(TypedDict): + type: Literal["rejected"] + rejectReason: str + +class AgentServiceError_timeout(TypedDict): + type: Literal["timeout"] + +class AgentServiceError_noPendingRequest(TypedDict): + type: Literal["noPendingRequest"] + +class AgentServiceError_notDRAddress(TypedDict): + type: Literal["notDRAddress"] + +class AgentServiceError_badSignature(TypedDict): + type: Literal["badSignature"] + +AgentServiceError = ( + AgentServiceError_rejected + | AgentServiceError_timeout + | AgentServiceError_noPendingRequest + | AgentServiceError_notDRAddress + | AgentServiceError_badSignature +) + +AgentServiceError_Tag = Literal["rejected", "timeout", "noPendingRequest", "notDRAddress", "badSignature"] + class AutoAccept(TypedDict): acceptIncognito: bool @@ -1420,6 +1446,7 @@ class Contact(TypedDict): chatTs: NotRequired[str] # ISO-8601 timestamp preparedContact: NotRequired["PreparedContact"] contactRequestId: NotRequired[int] # int64 + contactRequest: NotRequired["UserContactRequestRef"] contactGroupMemberId: NotRequired[int] # int64 contactGrpInvSent: bool groupDirectInv: NotRequired["GroupDirectInvitation"] @@ -1469,7 +1496,7 @@ class ContactShortLinkData(TypedDict): business: bool localBadge: NotRequired["LocalBadge"] -ContactStatus = Literal["active", "deleted", "deletedByUser"] +ContactStatus = Literal["active", "deleted", "deletedByUser", "rejected"] class ContactUserPref_contact(TypedDict): type: Literal["contact"] @@ -2761,6 +2788,10 @@ class SMPAgentError_A_QUEUE(TypedDict): type: Literal["A_QUEUE"] queueErr: str +class SMPAgentError_A_SERVICE(TypedDict): + type: Literal["A_SERVICE"] + serviceError: "AgentServiceError" + SMPAgentError = ( SMPAgentError_A_MESSAGE | SMPAgentError_A_PROHIBITED @@ -2769,9 +2800,10 @@ SMPAgentError = ( | SMPAgentError_A_CRYPTO | SMPAgentError_A_DUPLICATE | SMPAgentError_A_QUEUE + | SMPAgentError_A_SERVICE ) -SMPAgentError_Tag = Literal["A_MESSAGE", "A_PROHIBITED", "A_VERSION", "A_LINK", "A_CRYPTO", "A_DUPLICATE", "A_QUEUE"] +SMPAgentError_Tag = Literal["A_MESSAGE", "A_PROHIBITED", "A_VERSION", "A_LINK", "A_CRYPTO", "A_DUPLICATE", "A_QUEUE", "A_SERVICE"] class SecurityCode(TypedDict): securityCode: str @@ -3537,6 +3569,11 @@ class UserContactRequest(TypedDict): pqSupport: bool welcomeSharedMsgId: NotRequired[str] requestSharedMsgId: NotRequired[str] + rejectionSupported: bool + +class UserContactRequestRef(TypedDict): + contactRequestId: int # int64 + rejectionSupported: bool class UserInfo(TypedDict): user: "User" diff --git a/plans/2026-07-22-service-rpc-chat.md b/plans/2026-07-22-service-rpc-chat.md new file mode 100644 index 0000000000..8bec5fd7b2 --- /dev/null +++ b/plans/2026-07-22-service-rpc-chat.md @@ -0,0 +1,270 @@ +# Service RPC in simplex-chat + +Add chat-level support for the agent's Service RPC (single request → single response over a +DR contact address) and reasoned contact rejection, on top of the agent API shipped in simplexmq +`ccc1d7f8` (pinned in `cabal.project:24`). + +## Agent API available (simplexmq HEAD, verified) + +- `sendServiceRequest :: AgentClient -> NetworkRequestMode -> UserId -> ConnectionRequestUri 'CMContact -> MsgBody -> AE MsgBody` — sync; fails fast on server down; blocks and returns the response. +- `sendServiceRequestAsync :: AgentClient -> UserId -> ConnectionRequestUri 'CMContact -> MsgBody -> AE MsgBody` — enqueues a retried JOIN, blocks on the same TMVar, returns the response. +- `sendServiceReply / sendServiceReplyAsync :: ... -> UserId -> InvitationId -> MsgBody -> AE ()`. +- `rejectServiceRequest / rejectServiceRequestAsync :: ... -> UserId -> InvitationId -> Maybe ByteString -> AE ()`. +- `rejectContact :: AgentClient -> NetworkRequestMode -> UserId -> ConfirmationId -> Maybe ByteString -> AE ()` (already called at `Commands.hs:1448` with `Nothing`). +- Events: `SREQ :: InvitationId -> MsgBody -> AEvent AEConn`; `RJCT :: ConnInfo -> AEvent AEConn` (reason bytes). +- Errors: `A_SERVICE (serviceError :: AgentServiceError)`, `AgentServiceError = ASERejected {rejectReason :: String} | ASETimeout | ASENoPendingRequest | ASENotDRAddress`. +- A service request to a non-DR contact address fails fast with `A_SERVICE ASENotDRAddress` (agent guards this in `serviceRequest_`). + +## Agent-side changes required (simplexmq) + +These must be implemented in the agent first (rebuild, bump the chat pin): +1. **Per-call request timeout** (D1): add `Maybe NominalDiffTime` to `sendServiceRequest` / + `sendServiceRequestAsync` and thread it into `serviceRequest_`, overriding + `serviceRequestTimeout` from config when `Just` (`reqTimeout <- maybe (asks $ serviceRequestTimeout . config) pure timeout_`). +2. **Rejection-reason capability on `REQ`** (item 8): add a `Bool` to `REQ` indicating whether a + rejection reason can be delivered (i.e. the stored invitation is `CRInvitationDR`), sourced at + `smpContactRequest`. Update all `REQ` consumers (chat Subscriber.hs:1398 and any others). + +## Cross-cutting decisions (resolved) + +### D1. Blocking, with per-call timeout override +Commands are blocking (confirmed). `APISendServiceRequest` uses `sendServiceRequestAsync` and blocks +until the response, but takes an explicit `Maybe NominalDiffTime` that overrides the default +`serviceRequestTimeout` per call (agent change #1). Command processing is per-request in +`processChatCommand`; a blocked command does not stall the agent-event subscriber. + +### D2. No persistence in chat (confirmed) +Do not persist incoming service requests. Carry the agent `InvitationId` (as `AgentInvId`) through +`CEvtServiceRequest` and back through `APISendServiceResponse`. The agent already persists the +invitation and deletes it on `serviceResponseTimeout`. + +### D3. Payload types (resolved) — all `J.Object` +All four service-RPC payloads are `J.Object` (`Data.Aeson.Object`), to guarantee valid JSON on the +wire in both directions: +- `APISendServiceRequest.request :: J.Object` (UI → wire: `J.encode`). +- `CEvtServiceRequest.requestData :: J.Object` (wire → service: parse the received `MsgBody` as a + JSON object; a non-object / invalid JSON body is an error). +- `APISendServiceResponse.responseData :: J.Object` (service → wire). +- `CRServiceResponse.responseData :: J.Object` (wire → requester: parse the response `MsgBody`). +The agent still carries opaque `MsgBody`; the chat layer encodes/decodes `J.Object` at each boundary. + +## Item-by-item plan + +### 1 + 2. APISendServiceRequest → CRServiceResponse +- `ChatCommand` (Controller.hs ~534, near the connect commands): + `APISendServiceRequest {userId :: UserId, sendTarget :: ConnectTarget 'CMContact, requestTimeout :: Maybe NominalDiffTime, request :: J.Object}`. + Accept the mode-indexed `ConnectTarget 'CMContact` (parser must reject `CMInvitation` by producing + a parse/command error) — see target resolution below. +- `ChatResponse` (Controller.hs ~830): `CRServiceResponse {user :: User, responseData :: J.Object}` + (parse the agent's response `MsgBody` as a JSON object; invalid → chat error). +- Parser (`chatCommandP`, Commands.hs ~5479): `"/_service_request "` reading userId + a + `ConnectTarget 'CMContact` + optional timeout + the JSON request (`jsonP`). Parse the mode-indexed + target with a `ConnectionModeI m => Parser (ConnectTarget m)` (or `StrEncoding (ConnectTarget m)` + instance) that parses `AConnectTarget` (`strP`, Types.hs:1826) and narrows via + `testEquality m (sConnectionMode @m)`, failing on mode mismatch — the pattern of `connReqUriP'` + (Protocol.hs:1236). +- Processing (Commands.hs): resolve `sendTarget` to a `ConnectionRequestUri 'CMContact` (target + resolution below), then + `resp <- withAgent $ \a -> sendServiceRequestAsync a (aUserId user) cReq requestTimeout (LB.toStrict $ J.encode request)` + and return `CRServiceResponse user resp`. + `withAgent :: (AgentClient -> ExceptT AgentErrorType IO a) -> CM a` (Controller.hs:1762); + `sendServiceRequestAsync` returns `AE MsgBody`, so this is `CM MsgBody`. `aUserId user` is the + agent user id (same as the existing `rejectContact a NRMInteractive (aUserId user) invId ...` at + Commands.hs:1448). Signature assumes agent change #1 added `Maybe NominalDiffTime`. +- Outcomes surfaced to the caller: **success** → `CRServiceResponse`; **timeout** → + `A_SERVICE ASETimeout` (also the outcome when the responder does not support service requests — it + drops the request without a reply, so the sender waits out `requestTimeout`); **non-DR target** → + `A_SERVICE ASENotDRAddress`. Chat maps these `ChatErrorAgent` cases to chat errors; it does not + pre-check DR. (`ASERejected` is not produced: chat has no service-rejection-with-reason.) + +Target resolution (`ConnectTarget 'CMContact` → `ConnectionRequestUri 'CMContact`). Name types are +`NTContact` and `NTPublicGroup` (Commands.hs:4412–4413 — a channel is `NTPublicGroup`): +- `CTFullContact cr` → `cr` directly (agent checks DR; no resolution). +- `CTShortContact (CTName SimplexNameInfo {nameType = NTContact, nameDomain = d})` → resolve the + name to its contact link and then to `CRContactUri`. Reuse the resolution used by `connectPlan` + (Commands.hs:4306; the `SCMContact`/`CTName` branch at 4328–4412): `resolveSimplexName a nm + (aUserId user) d` → `NameRecord`, take `nrSimplexContact` (the contact link; `firstNameLink + CCTContact`), then `getShortLinkConnReq nm user sLnk` (Commands.hs:2384) → `cReq`. Require a + contact link; reject if it resolves only to a channel. +- `CTShortContact (CTName {nameType = NTPublicGroup})` → **fail** ("channel name not supported"). +- `CTShortContact (CTLink shortLink)` → **supported**: resolve via `getShortLinkConnReq nm user + shortLink` (Commands.hs:2384) → `cReq` (a contact short link resolves to a contact URI). +- `CTDomain _` → **fail** ("domain not supported"). + +### 3 + 7. SREQ event → CEvtServiceRequest, gated by start flag +- Extend `StartChat` (Controller.hs:351) with `processServiceRequests :: Bool` (default `False`); + keep `mainApp, enableSndFiles`. All `StartChat` sites use record syntax (patterns safe): + Controller.hs:696 (`StartChat {}` wildcard — no change); Commands.hs:552 (pattern — add the field + to use it); Commands.hs:5406 (full-form construction — parse and add the field); + Commands.hs:5407 (`/_start` bare — add `processServiceRequests = False`). +- Thread through `startChatController` (Commands.hs:219 — currently `Bool -> Bool -> CM' (Async ())`; + becomes `Bool -> Bool -> Bool -> ...`), and add a runtime flag to `ChatController` + (Controller.hs:277) mirroring `subscriptionMode :: TVar SubscriptionMode` (:291): + `processServiceRequests :: TVar Bool`. Set it in `startChatController` via `chatWriteVar'` + (`chatReadVar`/`chatWriteVar` at Controller.hs:1647/1655 take `ChatController -> TVar a`). + Both callers pass the new arg: `Core.hs:93` (`startChatController True True` → add `False`) and + `Commands.hs:555` (`startChatController mainApp enableSndFiles` → add the new `StartChat` field). +- The start flag answers exactly one question: **does this instance support service requests?** +- Handle `SREQ` in `processContactConnMessage` (Subscriber.hs:1397 — the `UserContact` entity + handler), immediately after `REQ` (:1398), since SREQ arrives on the published contact address + like REQ: + `SREQ invId payload -> chatReadVar processServiceRequests >>= \case` + `True -> toView $ CEvtServiceRequest user (AgentInvId invId) payload` + `False -> withAgent $ \a -> rejectServiceRequest a NRMBackground (aUserId user) invId Nothing` + When unsupported, **actively reject** (drop) the request so pending service requests do not + accumulate on the agent side until timeout. No reason is sent. + Note: unlike `REQ` (which does `parseChatMessage' conn connInfo`), the SREQ payload is **not** a + `ChatMessage` — it is the raw request body; do not parse it as one. +- `ChatEvent` (Controller.hs:924): `CEvtServiceRequest {user :: User, requestId :: AgentInvId, requestData :: J.Object}` + (parse the received `MsgBody` as a JSON object). + +### 4. APISendServiceResponse +- `ChatCommand`: `APISendServiceResponse {userId :: UserId, requestId :: AgentInvId, responseData :: J.Object}` (per D3). +- Processing: `withAgent $ \a -> sendServiceReplyAsync a "" (aUserId user) invId (LB.toStrict $ J.encode responseData)` + (async reply, retried and delivery-bounded by `serviceResponseTimeout`). Return `CRCmdOk`. + +### 5. (removed — no service-rejection API) +Per spec, service-request rejection is only the automatic drop in item 3 (when the instance does not +support service requests). There is no app-driven reject API and no service-rejection-with-reason; +`ContactRejectionReason` is **not** used for service requests. + +### 6. ContactRejectionReason + reasoned APIRejectContact +- New type (Types.hs), modelled on `GroupRejectionReason` (Types.hs:968): + `data ContactRejectionReason = CRRUserRejected | CRRUnknown {text :: Text}` with a `StrEncoding` + instance (`CRRUserRejected -> "user_rejected"`, `CRRUnknown t -> encodeUtf8 t`, and an + `A.takeByteString` fallback → `CRRUnknown`) and JSON via `strToJSON`/`strParseJSON` — the reason + encodes to a JSON string. The `strEncode` bytes are the `reason` sent in the agent `AgentRejection`. +- Extend `APIRejectContact {contactReqId :: Int64}` (Controller.hs:411) → + `APIRejectContact {contactReqId :: Int64, notify :: Bool}`. `notify` chooses whether to send the + rejection to the requester; the reason is fixed `CRRUserRejected` (the only variant), so the API + has no reason parameter (mirrors `ChatDeleteMode {notify :: Bool}`, Controller.hs:1100). Call sites + (1432/2475 are positional → break on the new field): + - Controller.hs:411 — definition. + - Commands.hs:1432 — pattern `APIRejectContact connReqId ->` → add `notify`. + - Terminal `RejectContact` (Controller.hs:564, parser :5712, handler :2473) gains `notify :: Bool` + (so a notifying rejection is CLI-testable); :2475 forwards it to `APIRejectContact`. + - Commands.hs:5480 — parser `"/_reject "` → parse `notify` (on/off); :5712 (`/reject @name`) likewise. +- Processing (`rejectCReq`, Commands.hs:1438): `notify = True` sends `CRRUserRejected`, `False` sends + nothing. **Send the rejection before deleting** the local records: currently deleted at 1444–1446, + then `rejectContact … Nothing` at :1448; reorder so + `rejectContact a NRMInteractive (aUserId user) invId (if notify then Just (strEncode CRRUserRejected) else Nothing)` + runs first, then the deletion. A notifying rejection of a non-DR request makes the agent throw + `CMD PROHIBITED`; because the send is first, nothing is deleted — surface the error so the UI shows + the alert and offers a silent (non-notifying) retry. `rejectionSupported` (item 8) drives whether + the UI offers the notify option up front. +- Requester side (currently no `RJCT` handler in chat): add `RJCT reasonBytes` in `processDirectMessage` + (Subscriber.hs:441), the `RcvDirectMsgConnection conn contact_` handler, which already has + `contact_ :: Maybe Contact` — the contact to mark. (Not `processContactConnMessage`, which handles + the responder's `UserContact` address.) Handling: + (1) `strDecode` the `ContactRejectionReason` from `reasonBytes` (decode failure → `Nothing`); + (2) set the contact's status to `CSRejected`; (3) emit `CEvtContactRequestRejected {user, contact, + rejectionReason :: Maybe ContactRejectionReason}`. The reason is reported in the event only; it is + not persisted. +- Add `CSRejected` to `ContactStatus` (Types.hs:315), with `textEncode`/`textDecode` `"rejected"` + (mirrors `GSMemRejected` Types.hs:1356, `RSRejected` Types/Shared.hs:95). No new column — the + existing `contact_status` text column stores it. + +### 8. UI signal: is reasoned rejection possible for a contact request? +Reasoned rejection needs a DR channel back to the requester (the agent's `rejectContact reason` path, +`prepareReply`, requires the stored invitation's `connReq` to be `CRInvitationDR`; a non-DR request +throws "connection has no double ratchet to send reply"). This is per-request, so it must flow from +the agent invitation. +- **Agent change #2**: add a `Bool` to `REQ` signalling rejection-reason-possible, sourced from the + invitation's `connReq` (DR) at `smpContactRequest`; update all `REQ` consumers. +- Agent change #2: add a `Bool` to `REQ` signalling rejection-reason capability, sourced from the + invitation's `connReq` (DR) at `smpContactRequest`; update all `REQ` consumers. +- Chat: `profileContactRequest` (Subscriber.hs:1470) → `createOrUpdateContactRequest` stores the flag + on the contact-request row (new column), surfaced two ways: + 1. On `UserContactRequest {rejectionSupported :: Bool}` (Types.hs:371), in + `CEvtReceivedContactRequest` (Controller.hs:946). + 2. On the prepared `Contact` that represents the request (`REContact ct`, Subscriber.hs:1482/1490). + +Prepared-Contact representation: +- New type `data UserContactRequestRef = UserContactRequestRef { contactRequestId :: Int64, rejectionSupported :: Bool }`. +- Add `contactRequest :: Maybe UserContactRequestRef` to `Contact` (Types.hs:193). Keep the existing + `contactRequestId :: Maybe Int64` (:207) — mobile/desktop remote-protocol compatibility requires it + in Haskell — but UI switches to `contactRequest` (whose ref carries the id), stopping use of the + bare `contactRequestId`. +- Populate via LEFT JOIN, not denormalization: `getContact_` (Store/Direct.hs:963) already selects + `ct.contact_request_id` and joins `contact_profiles`/`connections`; add + `LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = ct.contact_request_id`, select + `cr2.rejection_supported`. `getUserContacts` (:817) calls `getContact` per id, so this is one extra + join per contact query — no batched-query change. Build + `contactRequest = UserContactRequestRef <$> ct.contact_request_id <*> rejectionSupported_` — `Just` + only while the request row exists (pending); `Nothing` once accepted/rejected (row deleted at + Direct.hs:891). +- DB: add `rejection_supported` to `contact_requests`. + +## Implementation order +1. Agent (simplexmq): (a) `Maybe NominalDiffTime` on `sendServiceRequest[Async]` (D1); (b) `Bool` on + `REQ` for rejection-reason capability (item 8). Rebuild; bump chat's pin. +2. Chat types: `ContactRejectionReason` (StrEncoding); `UserContactRequestRef`; `CSRejected` in + `ContactStatus`; extend `StartChat` (+`processServiceRequests`), `APIRejectContact`/`RejectContact` + (+`notify`); add `APISendServiceRequest`, `APISendServiceResponse`; add `CRServiceResponse`, + `CEvtServiceRequest`, `CEvtContactRequestRejected`; `ChatController` `processServiceRequests :: TVar Bool`; + `rejectionSupported` on `UserContactRequest`; `contactRequest :: Maybe UserContactRequestRef` on + `Contact` (keep `contactRequestId`). +3. DB migration: `rejection_supported` on `contact_requests`. (`CSRejected` reuses the existing + `contact_status` text column — no migration.) +4. Parsers in `chatCommandP`. +5. Processing in `Library/Commands.hs` (send request, send response, reasoned reject-contact, start + flag threading). +6. Event handling in `Library/Subscriber.hs` (SREQ: gate + active-reject when off; REQ: store the + capability flag; RJCT: decode reason, set contact `CSRejected`, emit event). +7. Store: `getContact_` adds `LEFT JOIN contact_requests` for `rejection_supported`, builds + `contactRequest`. +8. `View.hs` rendering for the new responses/events. +9. Tests (functional: send request/response, unsupported-instance drop → timeout, reasoned reject + + requester `CSRejected`, non-DR reasoned reject error). +Scope: Haskell core, `View.hs`, tests. Bot API is autogenerated (no manual mobile/TS work). + +## Verified against code (review loop) +- Agent API names/signatures at simplexmq HEAD `ccc1d7f8` (Agent.hs:508–537, Protocol.hs:418–420, + 2202); chat pins that exact tag (cabal.project:24). Agent changes #1/#2 are additive to that. +- `ConnectTarget 'CMContact` = `CTFullContact | CTShortContact (CTName|CTLink) | CTDomain` + (Types.hs:1796–1802); parser `strP` at Types.hs:1826. Name types `NTContact`/`NTPublicGroup` + (Commands.hs:4412–4413). +- `AgentInvId = AgentInvId InvitationId` with `StrEncoding` (Types.hs:1699–1704); already the id type + on `UserContactRequest` (Types.hs:373). +- `withAgent :: (AgentClient -> ExceptT AgentErrorType IO a) -> CM a` (Controller.hs:1762); + `CRCmdOk {user_ :: Maybe User}` (Controller.hs:813). +- `SREQ`/`REQ` are received in `processContactConnMessage` on the `UserContact` entity + (Subscriber.hs:1397–1398); events via `toView`. No `RJCT` handler exists. +- Commands are text-parsed (`parseChatCommand = A.parseOnly chatCommandP`, Commands.hs:411), not JSON + — so a `ConnectTarget 'CMContact` field is fine (parser uses `AConnectTarget`'s `strP` then requires + `SCMContact`). `ChatDeleteMode {notify :: Bool}` (Controller.hs:1100) is the notify-choice pattern. +- `SREQ invId payload` (Agent.hs:3968) delivers the full request payload, parallel to `REQ … cInfo` + (:3936/:3965) — the requester's data reaches the service in the payload; `processDirectMessage` + (Subscriber.hs:441, `RcvDirectMsgConnection conn contact_`) has the `contact_` for RJCT. +- `StartChat` all record-syntax (Controller.hs:351/696, Commands.hs:552/5406/5407); + `startChatController` callers Core.hs:93 + Commands.hs:555; runtime flag pattern + `subscriptionMode :: TVar` (Controller.hs:291) via `chatReadVar`/`chatWriteVar` + (Controller.hs:1647/1655). +- `APIRejectContact` constructed/matched **positionally** (Commands.hs:1432, 2475) — adding a field + breaks both; parser at 5480; agent call at 1448. +- `ContactRejectionReason` follows `GroupRejectionReason` (Types.hs:968): `StrEncoding` + + `strToJSON`/`strParseJSON` (string JSON). +- Incoming contact request creates a prepared `Contact` (`REContact ct`, Subscriber.hs:1482/1490) + via `createOrUpdateContactRequest`; `Contact` has `preparedContact :: Maybe PreparedContact` + (Types.hs:206), `contactRequestId :: Maybe Int64` (:207); `PreparedContact` at :227; built by + `toContact` (Store/Direct.hs), read by `getContact` :960 / `getUserContacts` :817. +- `UserContactRequest` (Types.hs:371) ↔ `Contact` link is bidirectional (`contactId_` / `contactRequestId`). +- Rejected-state precedents: `GSMemRejected` (Types.hs:1356), `RSRejected` (Types/Shared.hs:95); + `ContactStatus` (Types.hs:315) has no rejected today. Requester prepared contact = `Contact` with + `ConnPrepared` connection (connectViaContact, Commands.hs:3761/3769). + +## Amendment: signed service requests + +Pairs with the agent-side signing (simplexmq `plans/2026-07-24-signed-service-requests.md`); pin bumped +to `0cc09fe5`. The agent constructs and verifies the Ed25519 signature — chat only carries the keys. + +- `APISendServiceRequest` gains `signKey :: Maybe C.PrivateKeyEd25519`; command syntax + `sign_key=` (string-encoded private key, via the enabled `StrEncoding (PrivateKey Ed25519)`), + threaded into `sendServiceRequestAsync`. Absent = unsigned (unchanged behaviour). +- `CEvtServiceRequest` gains `signerKey :: Maybe C.PublicKeyEd25519`, taken from the agent's `SREQ` + (the verified key, `Nothing` when unsigned); `View.hs` renders a `signed by ` line. +- An invalid signature arrives as the generic `A_SERVICE ASEBadSignature` agent error — no dedicated + chat event; the bot pattern-matches the error. +- Docs updated (commands / events / docs types). +- Test: `testSignedServiceRequest` — end-to-end, a signed request shows `signed by ` matching + the sender's key. diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 5d70ed7b94..7e422c51fe 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."efaad8e73436d60f5052f07dda6b71151ad5039b" = "1jczm6baqz34sn13jgp5srqjk3gnx90brsfrsqw29al769ssrvkv"; + "https://github.com/simplex-chat/simplexmq.git"."ee4dd0d8ded0f66f70a8890ce09d69e3610aa276" = "0zahz4011adavfz680wkcqvcrl2f9zjdx6dly6yqkcin4bpr6v3k"; "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/scripts/simplex-chat-reproduce-builds-android.sh b/scripts/simplex-chat-reproduce-builds-android.sh index 20c2fdf116..f8bb3224cc 100755 --- a/scripts/simplex-chat-reproduce-builds-android.sh +++ b/scripts/simplex-chat-reproduce-builds-android.sh @@ -118,9 +118,14 @@ check_apk() { verify_apk() { apk_name="$1" + # Release APKs are packaged by AGP (gradle :android:assembleRelease; AGP version is + # gradle.plugin.version in apps/multiplatform/gradle.properties), which zero-pads ZIP + # alignment. Do NOT add --pad-like-apksigner (standalone apksigner >= 35.0.0-rc1 uses + # the 0xd935 extra-field padding) unless AGP is bumped to a packager that uses it — + # otherwise apksigcopier aborts with "APK Signing Block offset < central directory offset". # https://github.com/obfusk/apksigcopier?tab=readme-ov-file#what-about-signatures-made-by-apksigner-from-build-tools--3500-rc1 - docker exec "${CONTAINER_NAME}" repro-apk zipalign --page-size 16 --pad-like-apksigner --replace "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \ - "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" + docker exec "${CONTAINER_NAME}" repro-apk zipalign --page-size 16 --replace "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \ + "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" docker exec "${CONTAINER_NAME}" mv "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" \ "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" diff --git a/simplex-chat.cabal b/simplex-chat.cabal index bf4dc7e10b..51f2ac8e91 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 7.0.0.11 +version: 7.1.0.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -152,6 +152,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles + Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection else exposed-modules: Simplex.Chat.Archive @@ -321,6 +322,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles + Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index b795ba9b9c..69e271e927 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -116,6 +116,7 @@ defaultChatConfig = inlineFiles = defaultInlineFilesConfig, autoAcceptFileSize = 0, showReactions = False, + showFullLinks = False, showReceipts = False, logLevel = CLLImportant, subscriptionEvents = False, @@ -153,11 +154,11 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, webPreviewConfig, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, deviceName, webPreviewConfig, highlyAvailable, yesToUpMigrations}, optFilesFolder, optTempDirectory, showReactions, showFullLinks, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations - config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, webPreviewConfig, highlyAvailable, confirmMigrations = confirmMigrations'} + config = cfg {logLevel, showReactions, showFullLinks, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, webPreviewConfig, highlyAvailable, confirmMigrations = confirmMigrations'} randomPresetServers <- chooseRandomServers presetServers' let rndSrvs = L.toList randomPresetServers operatorWithId (i, op) = (\o -> o {operatorId = DBEntityId i}) <$> pOperator op @@ -178,6 +179,7 @@ newChatController inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize subscriptionMode <- newTVarIO SMSubscribe + processServiceRequests <- newTVarIO False chatLock <- newEmptyTMVarIO entityLocks <- TM.emptyIO sndFiles <- newTVarIO M.empty @@ -223,6 +225,7 @@ newChatController inputQ, outputQ, subscriptionMode, + processServiceRequests, chatLock, entityLocks, sndFiles, diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 7284b72d62..dfb7418f3f 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -56,7 +56,7 @@ initializeBotAddress' logAddress cc = do Left (ChatErrorStore SEUserContactLinkNotFound) -> do when logAddress $ putStrLn "No bot address, creating..." -- TODO [short links] create short link by default - sendChatCmd cc CreateMyAddress >>= \case + sendChatCmd cc (CreateMyAddress Nothing) >>= \case Right (CRUserContactLinkCreated _ ccLink) -> showBotAddress ccLink _ -> putStrLn "can't create bot address" >> exitFailure _ -> putStrLn "unexpected response" >> exitFailure @@ -66,7 +66,7 @@ initializeBotAddress' logAddress cc = do putStrLn $ "Bot's contact address is: " <> B.unpack (maybe (strEncode uri) strEncode shortUri) when (isJust shortUri) $ putStrLn $ "Full contact address for old clients: " <> B.unpack (strEncode uri) let settings = AddressSettings {businessAddress = False, autoAccept = Just AutoAccept {acceptIncognito = False}, autoReply = Nothing} - void $ sendChatCmd cc $ SetAddressSettings settings + void $ sendChatCmd cc $ SetAddressSettings Nothing settings sendMessage :: ChatController -> Contact -> Text -> IO () sendMessage cc ct = sendComposedMessage cc ct Nothing . MCText diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 6a80f51a1e..af20d97bee 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -90,7 +90,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SMPServerWithAuth, SubscriptionMode (..), XFTPServer) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, TransportPeer (..), simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) @@ -152,6 +152,7 @@ data ChatConfig = ChatConfig inlineFiles :: InlineFilesConfig, autoAcceptFileSize :: Integer, showReactions :: Bool, + showFullLinks :: Bool, showReceipts :: Bool, subscriptionEvents :: Bool, hostEvents :: Bool, @@ -288,6 +289,7 @@ data ChatController = ChatController inputQ :: TBQueue String, outputQ :: TBQueue (Maybe RemoteHostId, Either ChatError ChatEvent), subscriptionMode :: TVar SubscriptionMode, + processServiceRequests :: TVar Bool, chatLock :: Lock, entityLocks :: TMap ChatLockEntity Lock, sndFiles :: TVar (Map Int64 Handle), @@ -347,7 +349,7 @@ data ChatCommand | SetClientService UserId ContactName Bool | APIDeleteUser {userId :: UserId, delSMPQueues :: Bool, viewPwd :: Maybe UserPwd} | DeleteUser UserName Bool (Maybe UserPwd) - | StartChat {mainApp :: Bool, enableSndFiles :: Bool} -- enableSndFiles has no effect when mainApp is True + | StartChat {mainApp :: Bool, enableSndFiles :: Bool, serviceRequests :: Bool} -- enableSndFiles has no effect when mainApp is True | CheckChatRunning | APIStopChat | APIActivateChat {restoreChat :: Bool} @@ -407,7 +409,9 @@ data ChatCommand | APIDeleteChat {chatRef :: ChatRef, chatDeleteMode :: ChatDeleteMode} -- currently delete mode settings are only applied to direct chats | APIClearChat {chatRef :: ChatRef} | APIAcceptContact {incognito :: IncognitoEnabled, contactReqId :: Int64} - | APIRejectContact {contactReqId :: Int64} + | APIRejectContact {contactReqId :: Int64, notify :: Bool} + | APISendServiceRequest {userId :: UserId, sendTarget :: ConnectTarget 'CMContact, requestTimeout :: Maybe NominalDiffTime, signKey :: Maybe (C.StoredPrivateKey 'C.Ed25519), request :: J.Object} + | APISendServiceResponse {userId :: UserId, requestId :: AgentInvId, responseData :: J.Object} | APISendCallInvitation ContactId CallType | SendCallInvitation ContactName CallType | APIRejectCall ContactId @@ -547,19 +551,20 @@ data ChatCommand | ClearContact ContactName | APIListContacts {userId :: UserId} | ListContacts - | APICreateMyAddress {userId :: UserId, server_ :: Maybe SMPServerWithAuth} - | CreateMyAddress + | APICreateMyAddress {userId :: UserId, server_ :: Maybe SMPServerWithAuth, pqRatchet :: Maybe Bool} + | CreateMyAddress {pqRatchet :: Maybe Bool} | APIDeleteMyAddress {userId :: UserId} | DeleteMyAddress | APIShowMyAddress {userId :: UserId} | ShowMyAddress - | APIAddMyAddressShortLink UserId + | APIAddMyAddressShortLink {userId :: UserId, pqRatchet :: Maybe Bool} + | APIRotateAddressRatchetKeys UserId | APISetProfileAddress {userId :: UserId, enable :: Bool} | SetProfileAddress Bool - | APISetAddressSettings {userId :: UserId, settings :: AddressSettings} - | SetAddressSettings AddressSettings + | APISetAddressSettings {userId :: UserId, pqRatchet :: Maybe Bool, settings :: AddressSettings} + | SetAddressSettings {pqRatchet :: Maybe Bool, settings :: AddressSettings} | AcceptContact IncognitoEnabled ContactName - | RejectContact ContactName + | RejectContact ContactName Bool | ForwardMessage {toChatName :: ChatName, fromContactName :: ContactName, forwardedMsg :: Text} | ForwardGroupMessage {toChatName :: ChatName, fromGroupName :: GroupName, fromMemberName_ :: Maybe ContactName, forwardedMsg :: Text} | ForwardLocalMessage {toChatName :: ChatName, forwardedMsg :: Text} @@ -826,6 +831,8 @@ data ChatResponse | CRUserContactLink {user :: User, contactLink :: UserContactLink} | CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink} | CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest, contact_ :: Maybe Contact} + | CRServiceResponse {user :: User, responseData :: J.Object} + | CRServiceReplyAccepted {user :: User, connectionId :: AgentConnId} | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} | CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool, msgSigned :: Bool} | CRGroupsList {user :: User, groups :: [GroupInfo]} @@ -937,11 +944,13 @@ data ChatEvent | CEvtUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} -- there is the same command response | CEvtGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} | CEvtBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact} - | CEvtSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} -- there is the same command response | CEvtContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} | CEvtGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember} | CEvtContactDeletedByContact {user :: User, contact :: Contact} | CEvtReceivedContactRequest {user :: User, contactRequest :: UserContactRequest, chat_ :: Maybe AChat} + | CEvtServiceRequest {user :: User, requestId :: AgentInvId, signerKey :: Maybe C.PublicKeyEd25519, requestData :: J.Object} + | CEvtServiceReplySent {connectionId :: AgentConnId} + | CEvtContactRequestRejected {user :: User, contact :: Contact, rejectionReason :: Maybe ContactRejectionReason} | CEvtAcceptingContactRequest {user :: User, contact :: Contact} -- there is the same command response | CEvtAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo} | CEvtContactRequestAlreadyAccepted {user :: User, contact :: Contact} diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index c8cf201421..c382a6dc8e 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -90,7 +90,7 @@ runSimplexChat :: ChatConfig -> ChatOpts -> User -> ChatController -> (User -> C runSimplexChat ChatConfig {testView} ChatOpts {coreOptions = CoreChatOpts {chatRelay, chatRelayServer, headless, maintenance}} u cc@ChatController {config = ChatConfig {chatHooks}} chat | maintenance = wait =<< async (chat u cc) | otherwise = do - a1 <- runReaderT (startChatController True True) cc + a1 <- runReaderT (startChatController True True False) cc when (chatRelay && not testView) $ askCreateRelayAddress cc u chatRelayServer headless forM_ (postStartHook chatHooks) ($ cc) a2 <- async $ chat u cc @@ -175,7 +175,7 @@ askCreateRelayAddress cc@ChatController {chatStore} user@User {userId} server_ h promptCreate = do ok <- if headless then pure True else onOffPrompt "Create relay address" True when ok $ - execChatCommand' (APICreateMyAddress userId server_) 0 `runReaderT` cc >>= \case + execChatCommand' (APICreateMyAddress userId server_ Nothing) 0 `runReaderT` cc >>= \case Right (CRUserContactLinkCreated _ address) -> do putStrLn "Chat relay address is created:" putStrLn $ addressStr address diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 2226f80fab..88cb4532ef 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -102,12 +102,12 @@ import Simplex.Messaging.Agent.Store.Interface (execSQL) import Simplex.Messaging.Agent.Store.Shared (upMigration) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations) -import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) +import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), pattern NRMInteractive, textToHostMode) import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.ShortLink as SL 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 PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Crypto.Ratchet (E2ERatchetParamsUri (..), InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQSupportOff, pattern PQSupportOn) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) @@ -216,9 +216,10 @@ videoFilePrefix :: String videoFilePrefix = "video_" -- enableSndFiles has no effect when mainApp is True -startChatController :: Bool -> Bool -> CM' (Async ()) -startChatController mainApp enableSndFiles = do +startChatController :: Bool -> Bool -> Bool -> CM' (Async ()) +startChatController mainApp enableSndFiles serviceRequests = do asks smpAgent >>= liftIO . resumeAgentClient + chatWriteVar' processServiceRequests serviceRequests unless mainApp $ chatWriteVar' subscriptionMode SMOnlyCreate users <- fromRight [] <$> runExceptT (withFastStore' getUsers) runExceptT (syncConnections' users) >>= \case @@ -548,10 +549,10 @@ processChatCommand cxt nm = \case checkDeleteChatUser user' withChatLock "deleteUser" $ deleteChatUser user' delSMPQueues DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ - StartChat {mainApp, enableSndFiles} -> withUser' $ \_ -> + StartChat {mainApp, enableSndFiles, serviceRequests} -> withUser' $ \_ -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning - _ -> checkStoreNotChanged . lift $ startChatController mainApp enableSndFiles $> CRChatStarted + _ -> checkStoreNotChanged . lift $ startChatController mainApp enableSndFiles serviceRequests $> CRChatStarted CheckChatRunning -> maybe CRChatStopped (const CRChatRunning) <$> chatReadVar agentAsync APIStopChat -> do ask >>= liftIO . stopChatController @@ -1429,24 +1430,49 @@ processChatCommand cxt nm = \case (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ mcSimple mc ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] - APIRejectContact connReqId -> withUser $ \user -> do + APIRejectContact connReqId notify -> withUser $ \user -> do uclId_ <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId withContactRequestLock "rejectContact" connReqId $ case uclId_ of Nothing -> rejectCReq user -- address was deleted Just uclId -> withUserContactLock "rejectContact" uclId $ rejectCReq user where rejectCReq user = do - (cReq@UserContactRequest {agentInvitationId = AgentInvId invId}, ct_) <- + cReq@UserContactRequest {agentInvitationId = AgentInvId invId, contactId_} <- + withFastStore $ \db -> getContactRequest db user connReqId + withAgent $ \a -> rejectContact a NRMInteractive (aUserId user) invId (if notify then Just (strEncode CRRUserRejected) else Nothing) + ct_ <- withFastStore $ \db -> do - cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId ct_ <- forM contactId_ $ \contactId -> do ct <- getContact db cxt user contactId deleteContact db user ct pure ct liftIO $ deleteContactRequest db user connReqId - pure (cReq, ct_) - withAgent (`rejectContact` invId) + pure ct_ pure $ CRContactRequestRejected user cReq ct_ + APISendServiceRequest userId sendTarget requestTimeout signKey request -> withUserId userId $ \user -> do + cReq <- resolveServiceTarget user sendTarget + respData <- withAgent $ \a -> sendServiceRequestAsync a (aUserId user) cReq requestTimeout (C.unStored <$> signKey) (LB.toStrict $ J.encode request) + resp <- either (const $ throwCmdError "invalid service response") pure $ J.eitherDecodeStrict' respData + pure $ CRServiceResponse user resp + where + resolveServiceTarget user = \case + CTFullContact cReq -> pure cReq + CTShortContact (CTLink sLnk) -> resolveShortLink sLnk + CTShortContact (CTName SimplexNameInfo {nameType, nameDomain}) -> case nameType of + NTContact -> resolveDomain nameDomain + _ -> throwCmdError "service request target must be a contact" + CTDomain d -> resolveDomain d + where + resolveDomain d = do + nr <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) d + case firstNameLink CCTContact (nrSimplexContact nr) of + Just sLnk -> resolveShortLink sLnk + Nothing -> throwChatError $ CESimplexDomainNotReady d SDENoValidLink + resolveShortLink sLnk = (\(_, _, cReq) -> cReq) <$> getShortLinkConnReq nm user sLnk + APISendServiceResponse userId requestId responseData -> withUserId userId $ \user -> do + let AgentInvId invId = requestId + connId <- withAgent $ \a -> sendServiceReplyAsync a "" (aUserId user) invId (LB.toStrict $ J.encode responseData) + pure $ CRServiceReplyAccepted user (AgentConnId connId) APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call ct <- withFastStore $ \db -> getContact db cxt user contactId @@ -1658,7 +1684,7 @@ processChatCommand cxt nm = \case r <- tryAllErrors $ getShortLinkConnReq nm user address case r of Left e -> failAt RTSGetLink e - Right (FixedLinkData {rootKey, linkConnReq = cReq}, cData) -> do + Right (FixedLinkData {rootKey}, cData, cReq) -> do relayProfile_ <- liftIO $ decodeLinkUserData cData case relayProfile_ of Nothing -> failAt RTSDecodeLink (ChatError $ CERelayTestError "no relay address link data") @@ -1667,8 +1693,8 @@ processChatCommand cxt nm = \case pure $ CRChatRelayTestResult user (Just relayProfile) (Just $ RelayTestFailure step e) lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case Nothing -> failWithProfile RTSConnect (ChatError $ CERelayTestError "invalid connection request") - Just (agentV, _) -> do - let chatV = agentToChatVersion agentV + Just _ -> do + let chatV = initialChatVersion subMode <- chatReadVar subscriptionMode connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff conn@Connection {connId = testCId} <- withFastStore $ \db -> @@ -1903,7 +1929,7 @@ processChatCommand cxt nm = \case gInfo@GroupInfo {groupProfile = p} <- withFastStore $ \db -> getGroupInfo db cxt user groupId case p of GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do - (_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk + (_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks}), _) <- getShortLinkConnReq' nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData gInfo' <- case groupSLinkData_ of Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData Nothing @@ -2071,7 +2097,7 @@ processChatCommand cxt nm = \case linkProfile <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True let userData = contactShortLinkData linkProfile {contactDomain = Nothing} Nothing userLinkData = UserInvLinkData userData - (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKPQOn subMode + (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation (Just userLinkData) Nothing IKUsePQ True subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn @@ -2113,7 +2139,7 @@ processChatCommand cxt nm = \case if short then Just . UserInvLinkData . (`contactShortLinkData` Nothing) <$> presentUserBadge newUser Nothing (userProfileDirect newUser Nothing Nothing True) else pure Nothing - (agConnId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userLinkData_ Nothing IKPQOn subMode + (agConnId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True False SCMInvitation userLinkData_ Nothing IKPQOn True subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId @@ -2249,7 +2275,7 @@ processChatCommand cxt nm = \case sLnk <- case connShortLink' connLinkToConnect of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to retrieve relays: no short link" - (FixedLinkData {linkConnReq = mainCReq@(CRContactUri crData), linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners, relays})) <- getShortLinkConnReq nm user sLnk + (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners, relays}), mainCReq@(CRContactUri crData e2e)) <- getShortLinkConnReq nm user sLnk groupSLinkData_ <- liftIO $ decodeLinkUserData cData -- Validate link entity ID matches group profile's publicGroupId (relay groups must have both) case groupSLinkData_ of @@ -2261,7 +2287,7 @@ processChatCommand cxt nm = \case -- Prepare group record once before connecting to relays (updatePreparedRelayedGroup): -- set group link info and incognito profile, generate and store membership keys incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - let cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} + let cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} e2e gVar <- asks random (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar gInfo' <- withFastStore $ \db -> do @@ -2381,7 +2407,7 @@ processChatCommand cxt nm = \case ccLink <- case contactLink of Just (CLFull cReq) -> pure $ CCLink cReq Nothing Just (CLShort sLnk) -> do - (FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user sLnk + (_, _, cReq) <- getShortLinkConnReq nm user sLnk pure $ CCLink cReq $ Just sLnk Nothing -> throwCmdError "no address in contact profile" connectContactViaAddress user incognito ct ccLink `catchAllErrors` \e -> do @@ -2399,7 +2425,7 @@ processChatCommand cxt nm = \case CRContactsList user <$> withFastStore' (\db -> getUserContacts db cxt user) ListContacts -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIListContacts userId - APICreateMyAddress userId server_ -> withUserId userId $ \user@User {userChatRelay} -> do + APICreateMyAddress userId server_ pqRatchet_ -> withUserId userId $ \user@User {userChatRelay} -> do withFastStore' (\db -> runExceptT $ getUserAddress db user) >>= \case Left SEUserContactLinkNotFound -> pure () Left e -> throwError $ ChatErrorStore e @@ -2408,20 +2434,25 @@ processChatCommand cxt nm = \case gVar <- asks random rootKey@(rootPubKey, rootPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar let entityId = C.sha256Hash $ C.pubKeyBytes rootPubKey - (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True Nothing server_ + -- TODO [address DR] remove this option and switch to IKUsePQ True + let (pqInitKeys, useDR) = case pqRatchet_ of + Just True -> (IKUsePQ, True) + Just False -> (IKPQOn, True) + Nothing -> (IKPQOn, False) + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True Nothing pqInitKeys useDR server_ ccLink' <- shortenCreatedLink ccLink -- TODO [relays] relay: add identity, key to link data? userData <- if isTrue userChatRelay then pure $ relayShortLinkData (userProfileDirect user Nothing Nothing True) else (`contactShortLinkData` Nothing) <$> presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True) - let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} - connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOn subMode + let userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData, ratchetKeys = Nothing} + connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData subMode let ccLink'' = if isTrue userChatRelay then setShortLinkType CCTRelay ccLink' else ccLink' withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode rootPrivKey pure $ CRUserContactLinkCreated user ccLink'' - CreateMyAddress -> withUser $ \User {userId} -> - processChatCommand cxt nm $ APICreateMyAddress userId Nothing + CreateMyAddress ratchetKeys_ -> withUser $ \User {userId} -> + processChatCommand cxt nm $ APICreateMyAddress userId Nothing ratchetKeys_ APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do conn <- withFastStore $ \db -> getUserAddressConnection db cxt user withChatLock "deleteMyAddress" $ do @@ -2439,8 +2470,12 @@ processChatCommand cxt nm = \case CRUserContactLink user <$> withFastStore (`getUserAddress` user) ShowMyAddress -> withUser' $ \User {userId} -> processChatCommand cxt nm $ APIShowMyAddress userId - APIAddMyAddressShortLink userId -> withUserId' userId $ \user -> - CRUserContactLink user <$> (withFastStore (`getUserAddress` user) >>= setMyAddressData user) + APIAddMyAddressShortLink userId pqRatchet_ -> withUserId' userId $ \user -> do + -- TODO [address DR] remove the option, and use IKUsePQ + let pqInitKeys = (\case True -> IKUsePQ; False -> IKPQOn) <$> pqRatchet_ + CRUserContactLink user <$> (withFastStore (`getUserAddress` user) >>= setMyAddressData False pqInitKeys user) + APIRotateAddressRatchetKeys userId -> withUserId' userId $ \user -> + CRUserContactLink user <$> (withFastStore (`getUserAddress` user) >>= setMyAddressData True (Just IKUsePQ) user) APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} updateProfile_ user p' True $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing @@ -2451,7 +2486,7 @@ processChatCommand cxt nm = \case updateProfile_ user p' True $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl SetProfileAddress onOff -> withUser $ \User {userId} -> processChatCommand cxt nm $ APISetProfileAddress userId onOff - APISetAddressSettings userId settings@AddressSettings {businessAddress, autoAccept} -> withUserId userId $ \user -> do + APISetAddressSettings userId pqRatchet_ settings@AddressSettings {businessAddress, autoAccept} -> withUserId userId $ \user -> do ucl@UserContactLink {userContactLinkId, shortLinkDataSet, addressSettings} <- withFastStore (`getUserAddress` user) forM_ autoAccept $ \AutoAccept {acceptIncognito} -> do when (shortLinkDataSet && acceptIncognito) $ throwCmdError "incognito not allowed for address with short link data" @@ -2460,17 +2495,18 @@ processChatCommand cxt nm = \case then pure $ CRUserContactLinkUpdated user ucl else do let ucl' = ucl {addressSettings = settings} - ucl'' <- if shortLinkDataSet then setMyAddressData user ucl' else pure ucl' + pqInitKeys = (\case True -> IKUsePQ; False -> IKPQOn) <$> pqRatchet_ + ucl'' <- if shortLinkDataSet then setMyAddressData False pqInitKeys user ucl' else pure ucl' withFastStore' $ \db -> updateUserAddressSettings db userContactLinkId settings pure $ CRUserContactLinkUpdated user ucl'' - SetAddressSettings settings -> withUser $ \User {userId} -> - processChatCommand cxt nm $ APISetAddressSettings userId settings + SetAddressSettings pqRatchet_ settings -> withUser $ \User {userId} -> + processChatCommand cxt nm $ APISetAddressSettings userId pqRatchet_ settings AcceptContact incognito cName -> withUser $ \User {userId} -> do connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName processChatCommand cxt nm $ APIAcceptContact incognito connReqId - RejectContact cName -> withUser $ \User {userId} -> do + RejectContact cName notify -> withUser $ \User {userId} -> do connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand cxt nm $ APIRejectContact connReqId + processChatCommand cxt nm $ APIRejectContact connReqId notify ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg @@ -2679,7 +2715,7 @@ processChatCommand cxt nm = \case let entityId = C.sha256Hash $ C.pubKeyBytes rootPubKey crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with entityId as linkEntityId (no server request) - (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) Nothing + (ccLink, preparedParams) <- withAgent $ \a -> prepareConnectionLink a (aUserId user) rootKey entityId True (Just crClientData) IKPQOff False Nothing ccLink' <- setShortLinkType CCTChannel <$> shortenCreatedLink ccLink sLnk <- case connShortLink' ccLink' of Just sl -> pure sl @@ -2690,9 +2726,9 @@ processChatCommand cxt nm = \case -- TODO [channel web] pass publicGroupAccess from owner's profile let groupProfile' = (groupProfile :: GroupProfile) {publicGroup = Just PublicGroupProfile {groupType = GTChannel, groupLink = sLnk, publicGroupId = B64UrlByteString entityId, publicGroupAccess = Nothing}} userData = encodeShortLinkData $ GroupShortLinkData {groupProfile = groupProfile', publicGroupData = Just (PublicGroupData 1)} - userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData} + userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData, ratchetKeys = Nothing} -- create connection with prepared link (single network call) - connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData IKPQOff subMode + connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData subMode let groupKeys = GroupKeys {publicGroupId = B64UrlByteString entityId, groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} setupLink gInfo = do -- TODO [relays] starting role should be communicated in protocol from owner to relays @@ -2758,7 +2794,7 @@ processChatCommand cxt nm = \case Nothing -> do gVar <- asks random subMode <- chatReadVar subscriptionMode - (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode + (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff True subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -3245,9 +3281,9 @@ processChatCommand cxt nm = \case groupLinkId <- GroupLinkId <$> drgRandomBytes 16 subMode <- chatReadVar subscriptionMode let userData = encodeShortLinkData $ GroupShortLinkData {groupProfile, publicGroupData = Nothing} - userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} + userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData, ratchetKeys = Nothing} crClientData = encodeJSON $ CRDataGroup groupLinkId - (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff subMode + (connId, ccLink) <- withAgent $ \a -> createConnection a nm (aUserId user) True True SCMContact (Just userLinkData) (Just crClientData) IKPQOff False subMode ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink gVar <- asks random gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode @@ -3283,11 +3319,11 @@ processChatCommand cxt nm = \case unless (groupFeatureUserAllowed SGFDirectMessages g) $ throwCmdError "direct messages not allowed" case memberConn m of Just mConn@Connection {peerChatVRange} -> do - unless (maxVersion peerChatVRange >= groupDirectInvVersion) $ throwChatError CEPeerChatVRangeIncompatible + unless (maxVersion peerChatVRange >= initialChatVersion) $ throwChatError CEPeerChatVRangeIncompatible when (isJust $ memberContactId m) $ throwCmdError "member contact already exists" subMode <- chatReadVar subscriptionMode -- TODO PQ should negotitate contact connection with PQSupportOn? - (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff subMode + (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a nm (aUserId user) True False SCMInvitation Nothing Nothing IKPQOff True subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart) @@ -3727,15 +3763,15 @@ processChatCommand cxt nm = \case lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq -- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan - Just (agentV, pqSup') -> do - let chatV = agentToChatVersion agentV + Just (_, pqSup') -> do + let chatV = initialChatVersion withFastStore' (\db -> getConnectionEntityByConnReq db cxt user cReqs) >>= \case Nothing -> joinNewConn chatV Just (RcvDirectMsgConnection conn@Connection {connStatus, contactConnInitiated, customUserProfileId} _ct_) | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV -- own connection link | connStatus == ConnPrepared -> do -- retrying join after error localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId - joinPreparedConn conn (fromLocalProfile <$> localIncognitoProfile) chatV + joinPreparedConn conn (fromLocalProfile <$> localIncognitoProfile) Just ent -> throwCmdError $ "connection is not RcvDirectMsgConnection: " <> show (connEntityInfo ent) where joinNewConn chatV = do @@ -3744,10 +3780,10 @@ processChatCommand cxt nm = \case connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ conn <- withFastStore' $ \db -> createDirectConnection' db userId connId ccLink contactId_ ConnPrepared incognitoProfile subMode chatV pqSup' - joinPreparedConn conn incognitoProfile chatV - joinPreparedConn conn incognitoProfile chatV = do + joinPreparedConn conn incognitoProfile + joinPreparedConn conn incognitoProfile = do profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user incognitoProfile Nothing True - dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend + dm <- encodeConnInfoPQ pqSup' $ XInfo profileToSend sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode let newStatus = if sqSecured then ConnSndReady else ConnJoined conn' <- withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus @@ -3757,7 +3793,7 @@ processChatCommand cxt nm = \case CRInvitationUri crData {crScheme = simplexChat} e2e ) connectViaContact :: User -> Maybe PreparedChatEntity -> IncognitoEnabled -> CreatedLinkContact -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> CM ConnectViaContactResult - connectViaContact user@User {userId} preparedEntity_ incognito (CCLink cReq@(CRContactUri crData@ConnReqUriData {crClientData}) sLnk) welcomeSharedMsgId msg_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do + connectViaContact user@User {userId} preparedEntity_ incognito (CCLink cReq@(CRContactUri crData@ConnReqUriData {crClientData} e2e) sLnk) welcomeSharedMsgId msg_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli -- groupLinkId is Nothing for business chats when (isJust msg_ && isJust groupLinkId) $ throwChatError CEConnReqMessageProhibited @@ -3789,8 +3825,8 @@ processChatCommand cxt nm = \case Just Connection {xContactId} -> connect' groupLinkId xContactId (groupLinkId $> Nothing) Nothing -> connect' groupLinkId Nothing (groupLinkId $> Nothing) where - cReqHash1 = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} - cReqHash2 = contactCReqHash $ CRContactUri crData {crScheme = simplexChat} + cReqHash1 = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} e2e + cReqHash2 = contactCReqHash $ CRContactUri crData {crScheme = simplexChat} e2e -- relay-group joins (only via connectToRelay) carry the target relay member in preparedEntity_; -- its memberId binds the join signature so a sibling relay can't replay it relayMemberId_ = case preparedEntity_ of @@ -3830,7 +3866,7 @@ processChatCommand cxt nm = \case -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq + let cReqHash = contactCReqHash cReq conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing Nothing pqSup ct' <- withStore $ \db -> getContact db cxt user contactId @@ -3851,15 +3887,14 @@ processChatCommand cxt nm = \case -- Save relayLink to re-use relay member record on retry (check by relayLink) relayMember <- withFastStore $ \db -> getCreateRelayForMember db cxt gVar user gInfo relayLink r <- tryAllErrors $ do - (fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink + (FixedLinkData {rootKey = relayKey, linkEntityId}, cData, cReq) <- getShortLinkConnReq nm user relayLink relayLinkData_ <- liftIO $ decodeLinkUserData cData relayMemberId <- case (relayLinkData_, linkEntityId) of (Just RelayShortLinkData {relayProfile = p}, Just entityId) -> do withFastStore $ \db -> updateRelayMemberData db cxt user relayMember (MemberId entityId) (MemberKey relayKey) p pure $ MemberId entityId _ -> throwChatError $ CEException "relay link: no relay link data or entity id" - let cReq = linkConnReq fd - relayLinkToConnect = CCLink cReq (Just relayLink) + let relayLinkToConnect = CCLink cReq (Just relayLink) void $ connectViaContact user (Just $ PCEGroup gInfo (relayMember {memberId = relayMemberId})) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing relayMember' <- withFastStore $ \db -> getGroupMember db cxt user (groupId' gInfo) (groupMemberId' relayMember) pure (relayLink, relayMember', r) @@ -3890,14 +3925,14 @@ processChatCommand cxt nm = \case -- 2) toggle enabled, address doesn't support PQ - PQSupportOn but without compression, with version range indicating support lift (withAgent' $ \a -> connRequestPQSupport a pqSup cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq - Just (agentV, _) -> do - let chatV = agentToChatVersion agentV + Just _ -> do + let chatV = initialChatVersion connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup pure (connId, chatV) mkXContactId :: Maybe XContactId -> CM XContactId mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> Maybe MemberId -> PQSupport -> CM Connection - joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do + joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do -- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile profileToSend <- presentUserBadge user incognitoProfile $ case gInfo_ of @@ -3907,7 +3942,7 @@ processChatCommand cxt nm = \case Just (Just gInfo) | useRelays' gInfo -> case relayMemberId_ of Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId" - _ -> encodeConnInfoPQ pqSup chatV $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ + _ -> encodeConnInfoPQ pqSup $ XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_ subMode <- chatReadVar subscriptionMode void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared ConnJoined @@ -3943,8 +3978,10 @@ processChatCommand cxt nm = \case setMyAddressData' :: User -> CM () setMyAddressData' user' = withFastStore' (\db -> runExceptT $ getUserAddress db user) >>= \case - Right ucl@UserContactLink {shortLinkDataSet} - | shortLinkDataSet -> void $ setMyAddressData user' ucl + Right ucl@UserContactLink {shortLinkDataSet, connLinkContact = CCLink {connFullLink = CRContactUri _ e2e}} + | shortLinkDataSet -> + let pqInitKeys = (\(_, E2ERatchetParamsUri _ _ _ pq) -> if isJust pq then IKUsePQ else IKPQOn) <$> e2e + in void $ setMyAddressData False pqInitKeys user' ucl _ -> pure () sendUpdateToContacts :: User -> [Contact] -> CM UserProfileUpdateSummary sendUpdateToContacts user' contacts = do @@ -3985,16 +4022,17 @@ processChatCommand cxt nm = \case ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} -> (conn, MsgFlags {notification = hasNotification XInfo_}, (vrValue msgBody, [msgId])) - setMyAddressData :: User -> UserContactLink -> CM UserContactLink - setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _, addressSettings} = do + setMyAddressData :: Bool -> Maybe InitialKeys -> User -> UserContactLink -> CM UserContactLink + setMyAddressData rotateKeys pqInitKeys user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _, addressSettings} = do conn <- withFastStore $ \db -> getUserAddressConnection db cxt user shortLinkProfile <- presentUserBadge user Nothing (userProfileDirect user Nothing Nothing True) -- TODO [short links] do not save address to server if data did not change, spinners, error handling let userData | isTrue userChatRelay = relayShortLinkData shortLinkProfile | otherwise = contactShortLinkData shortLinkProfile $ Just addressSettings - userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} - sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData Nothing) + userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData, ratchetKeys = Nothing} + -- TODO [address DR] remove parameter and switch to (Just IKUsePQ) after rotateKeys + sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData Nothing rotateKeys pqInitKeys) withFastStore' $ \db -> setUserContactLinkShortLink db userContactLinkId sLnk let autoAccept' = (\aa -> aa {acceptIncognito = False}) <$> autoAccept addressSettings ucl' = (ucl :: UserContactLink) {connLinkContact = CCLink connFullLink (Just sLnk), shortLinkDataSet = True, shortLinkLargeDataSet = BoolDef True, addressSettings = addressSettings {autoAccept = autoAccept'}} @@ -4212,11 +4250,11 @@ processChatCommand cxt nm = \case where addRelay :: UserChatRelay -> CM (UserChatRelay, Either ChatError GroupRelay) addRelay relay@UserChatRelay {address} = fmap (relay,) . tryAllErrors $ do - (FixedLinkData {linkConnReq = cReq}, _cData) <- getShortLinkConnReq nm user address + (_, _, cReq) <- getShortLinkConnReq nm user address lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOff cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq - Just (agentV, _) -> do - let chatV = agentToChatVersion agentV + Just _ -> do + let chatV = initialChatVersion gVar <- asks random subMode <- chatReadVar subscriptionMode connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff @@ -4311,7 +4349,7 @@ processChatCommand cxt nm = \case knownLinkPlans l' >>= \case Just (createdLink, p) -> pure (createdLink, Nothing, Nothing, p) Nothing -> do - (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {rootKey}, cData, cReq) <- getShortLinkConnReq nm user l' contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) let ov = verifyLinkOwner rootKey [] l sig_ invitationReqAndPlan cReq (Just l') contactSLinkData_ ov @@ -4365,7 +4403,7 @@ processChatCommand cxt nm = \case Nothing -> do when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally l' <- resolveSLink - (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' + (FixedLinkData {rootKey}, cData, cReq) <- getShortLinkConnReq nm user l' contactSLinkData_ <- mapM linkDataBadge =<< liftIO (decodeLinkUserData cData) let linkProfile_ = (\ContactShortLinkData {profile} -> profile) <$> contactSLinkData_ linkDomain_ = linkProfile_ >>= \Profile {contactDomain} -> claimDomain <$> contactDomain @@ -4425,13 +4463,13 @@ processChatCommand cxt nm = \case Nothing -> do when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally l' <- resolveSLink - (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays})) <- getShortLinkConnReq' nm user l' + (fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays}), cReq) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData if - | not direct && unsupportedGroupType groupSLinkData_ -> pure (con l' (linkConnReq fd), CPGroupLink (GLPUpdateRequired groupSLinkData_)) - | not direct && null relays -> pure (con l' (linkConnReq fd), CPGroupLink (GLPNoRelays groupSLinkData_)) + | not direct && unsupportedGroupType groupSLinkData_ -> pure (con l' cReq, CPGroupLink (GLPUpdateRequired groupSLinkData_)) + | not direct && null relays -> pure (con l' cReq, CPGroupLink (GLPNoRelays groupSLinkData_)) | otherwise -> do - let FixedLinkData {linkConnReq = cReq, linkEntityId, rootKey} = fd + let FixedLinkData {linkEntityId, rootKey} = fd linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId} let profilePGId = groupSLinkData_ >>= \GroupShortLinkData {groupProfile = GroupProfile {publicGroup}} -> fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId) publicGroup @@ -4470,14 +4508,14 @@ processChatCommand cxt nm = \case Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl' resolveKnownGroup g = do l' <- resolveSLink - (fd@FixedLinkData {rootKey = rk}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' nm user l' + (FixedLinkData {rootKey = rk}, cData@(ContactLinkData _ UserContactData {owners}), cReq) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData let ov = verifyLinkOwner rk owners l' sig_ glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners (g', updated) <- case groupSLinkData_ of Just sLinkData -> updateGroupFromLinkData user g sLinkData Nothing _ -> pure (g, False) - pure (con l' (linkConnReq fd), CPGroupLink (GLPKnown g' updated ov (ListDef glOwners))) + pure (con l' cReq, CPGroupLink (GLPKnown g' updated ov (ListDef glOwners))) -- resolve a name to its first contact/channel short link resolveNameLink :: SimplexNameInfo -> CM (ConnShortLink 'CMContact) resolveNameLink SimplexNameInfo {nameType, nameDomain} = do @@ -4544,15 +4582,14 @@ processChatCommand cxt nm = \case | otherwise -> CPInvitationLink (ILPConnecting Nothing) _ -> CPError $ ChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" contactOrGroupRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan - contactOrGroupRequestPlan user cReq@(CRContactUri crData) = do - let ConnReqUriData {crClientData} = crData - groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli + contactOrGroupRequestPlan user cReq@(CRContactUri ConnReqUriData {crClientData} _) = do + let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli case groupLinkId of Nothing -> contactRequestPlan user cReq Nothing Nothing Just _ -> groupJoinRequestPlan user cReq Nothing Nothing Nothing [] contactRequestPlan :: User -> ConnReqContact -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan - contactRequestPlan user (CRContactUri crData) cld ov = do - let cReqSchemas = contactCReqSchemas crData + contactRequestPlan user cReq cld ov = do + let cReqSchemas = contactCReqSchemas cReq cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas plan p = pure $ CPContactAddress p withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case @@ -4574,8 +4611,8 @@ processChatCommand cxt nm = \case Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Nothing Nothing [] Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkInfo -> Maybe GroupShortLinkData -> Maybe OwnerVerification -> [GroupLinkOwner] -> CM ConnectionPlan - groupJoinRequestPlan user (CRContactUri crData) linkInfo gld ov glOwners = do - let cReqSchemas = contactCReqSchemas crData + groupJoinRequestPlan user cReq linkInfo gld ov glOwners = do + let cReqSchemas = contactCReqSchemas cReq cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas plan p = pure $ CPGroupLink p withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db cxt user cReqSchemas) >>= \case @@ -4601,10 +4638,10 @@ processChatCommand cxt nm = \case | otherwise = plan $ GLPOk linkInfo gld ov where plan p = pure $ CPGroupLink p - contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) - contactCReqSchemas crData = - ( CRContactUri crData {crScheme = SSSimplex}, - CRContactUri crData {crScheme = simplexChat} + contactCReqSchemas :: ConnReqContact -> (ConnReqContact, ConnReqContact) + contactCReqSchemas (CRContactUri crData e2e) = + ( CRContactUri crData {crScheme = SSSimplex} e2e, + CRContactUri crData {crScheme = simplexChat} e2e ) -- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser, -- and short links are stored with server hostname schema, so they wouldn't match without it. @@ -4655,7 +4692,7 @@ processChatCommand cxt nm = \case forM (connShortLink' =<< connLinkInv) $ \_ -> do let userData = contactShortLinkData profile Nothing userLinkData = UserInvLinkData userData - shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing) + shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing False Nothing) updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM () updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db cxt user groupId @@ -5004,7 +5041,7 @@ verifyEntityDomain user nm nameType SimplexDomainClaim {domain = StrJSON domain, where verifyDomainProof :: SimplexDomainProof -> ShortLinkContact -> CM Bool verifyDomainProof SimplexDomainProof {linkOwnerId, presHeader, signature} sLnk@(CSLContact _ ct srv key) = do - (FixedLinkData {rootKey}, ContactLinkData _ UserContactData {owners}) <- getShortLinkConnReq nm user sLnk + (FixedLinkData {rootKey}, ContactLinkData _ UserContactData {owners}, _) <- getShortLinkConnReq nm user sLnk let ownerKey_ = case linkOwnerId of Nothing -> Just rootKey Just (StrJSON oid) -> ownerKey <$> find (\OwnerAuth {ownerId} -> ownerId == oid) owners @@ -5286,7 +5323,7 @@ runRelayGroupLinkChecks user = do forM_ relayGroups $ \gInfo@GroupInfo {groupProfile = gp} -> flip catchAllErrors eToView $ do case publicGroup gp of Just PublicGroupProfile {groupLink = sLnk} -> do - (_, ContactLinkData _ UserContactData {relays = relayLinks}) <- + (_, ContactLinkData _ UserContactData {relays = relayLinks}, _) <- getShortLinkConnReq' NRMBackground user sLnk gLink_ <- withStore' $ \db -> runExceptT $ getGroupLink db user gInfo case gLink_ of @@ -5404,8 +5441,9 @@ chatCommandP = "/_start " *> do mainApp <- "main=" *> onOffP enableSndFiles <- " snd_files=" *> onOffP <|> pure mainApp - pure StartChat {mainApp, enableSndFiles}, - "/_start" $> StartChat {mainApp = True, enableSndFiles = True}, + serviceRequests <- " service_requests=" *> onOffP <|> pure False + pure StartChat {mainApp, enableSndFiles, serviceRequests}, + "/_start" $> StartChat {mainApp = True, enableSndFiles = True, serviceRequests = False}, "/_check running" $> CheckChatRunning, "/_stop" $> APIStopChat, "/_app activate restore=" *> (APIActivateChat <$> onOffP), @@ -5478,7 +5516,9 @@ chatCommandP = "/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode), "/_clear chat " *> (APIClearChat <$> chatRefP), "/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal), - "/_reject " *> (APIRejectContact <$> A.decimal), + "/_reject " *> (APIRejectContact <$> A.decimal <*> (" notify=" *> onOffP <|> pure False)), + "/_service_request " *> (APISendServiceRequest <$> A.decimal <* A.space <*> strP <*> optional (" timeout=" *> (realToFrac <$> A.double)) <*> optional (" sign_key=" *> strP) <* A.space <*> jsonP), + "/_service_response " *> (APISendServiceResponse <$> A.decimal <* A.space <*> strP <* A.space <*> jsonP), "/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP), "/call " *> char_ '@' *> (SendCallInvitation <$> displayNameP <*> pure defaultCallType), "/_call reject @" *> (APIRejectCall <$> A.decimal), @@ -5697,19 +5737,20 @@ chatCommandP = ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), "/simplex" *> (ConnectSimplex <$> incognitoP), - "/_address " *> (APICreateMyAddress <$> A.decimal <*> optional (A.space *> strP)), - ("/address" <|> "/ad") $> CreateMyAddress, + "/_address " *> (APICreateMyAddress <$> A.decimal <*> optional (A.space *> strP) <*> optional (" pq_ratchet=" *> onOffP)), + ("/address" <|> "/ad") *> (CreateMyAddress <$> optional (" pq_ratchet=" *> onOffP)), "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), ("/delete_address" <|> "/da") $> DeleteMyAddress, "/_show_address " *> (APIShowMyAddress <$> A.decimal), ("/show_address" <|> "/sa") $> ShowMyAddress, - "/_short_link_address " *> (APIAddMyAddressShortLink <$> A.decimal), + "/_short_link_address " *> (APIAddMyAddressShortLink <$> A.decimal <*> optional (" pq_ratchet=" *> onOffP)), + "/_rotate_address_keys " *> (APIRotateAddressRatchetKeys <$> A.decimal), "/_profile_address " *> (APISetProfileAddress <$> A.decimal <* A.space <*> onOffP), ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), - "/_address_settings " *> (APISetAddressSettings <$> A.decimal <* A.space <*> jsonP), - "/auto_accept " *> (SetAddressSettings <$> autoAcceptP), + "/_address_settings " *> (APISetAddressSettings <$> A.decimal <*> optional (" pq_ratchet=" *> onOffP) <* A.space <*> jsonP), + "/auto_accept" *> (SetAddressSettings <$> optional (" pq_ratchet=" *> onOffP) <* A.space <*> autoAcceptP), ("/accept" <|> "/ac") *> (AcceptContact <$> incognitoP <* A.space <* char_ '@' <*> displayNameP), - ("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayNameP), + ("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayNameP <*> (" notify" $> True <|> pure False)), ("/markdown" <|> "/m") $> ChatHelp HSMarkdown, ("/welcome" <|> "/w") $> Welcome, "/set profile image file " *> (UpdateProfileImageFromFile <$> filePath), diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 6fb002eaee..ca6440d155 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -40,7 +40,7 @@ import Data.Foldable (foldr') import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) -import Data.List (find, foldl', mapAccumL, partition) +import Data.List (foldl', mapAccumL, partition) import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -96,7 +96,7 @@ import Simplex.Messaging.Compression (compressionLevel, limitDecompress') 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 PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding (smpEncode) import Simplex.Messaging.Encoding.String @@ -918,7 +918,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId pure (ct, conn, ExistingIncognito <$> incognitoProfile) profileToSend <- presentUserBadge user incognitoProfile $ userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True - dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend + dm <- encodeConnInfoPQ pqSup' $ XInfo profileToSend (ct,conn,) <$> withAgent (\a -> acceptContact a nm (aUserId user) (aConnId conn) True invId dm pqSup' subMode) acceptContactRequestAsync :: User -> Int64 -> Contact -> UserContactRequest -> Maybe IncognitoProfile -> CM Contact @@ -939,7 +939,7 @@ acceptContactRequestAsync Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs liftIO $ setCommandConnId db user cmdId connId getContact db cxt user contactId - agentAcceptContactAsync cmdId acId True cReqInvId (XInfo profileToSend) cReqPQSup chatV subMode + agentAcceptContactAsync cmdId acId True cReqInvId (XInfo profileToSend) cReqPQSup subMode pure ct' acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> Maybe GroupMember -> CM GroupMember @@ -992,7 +992,7 @@ acceptGroupJoinRequestAsync m <- withStore $ \db -> do liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode getGroupMemberById db cxt user groupMemberId - agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff subMode pure m acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupRejectionReason -> CM GroupMember @@ -1024,7 +1024,7 @@ acceptGroupJoinSendRejectAsync m <- withStore $ \db -> do liftIO $ createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode getGroupMemberById db cxt user groupMemberId - agentAcceptContactAsync cmdId acId False cReqInvId msg PQSupportOff chatV subMode + agentAcceptContactAsync cmdId acId False cReqInvId msg PQSupportOff subMode pure m acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember) @@ -1058,7 +1058,7 @@ acceptBusinessJoinRequestAsync withStore' $ \db -> do forM_ xContactId $ \xcId -> setBusinessChatAcceptedXContactId db gInfo xcId createJoiningMemberConnection db user uclId (cmdId, acId) chatV cReqChatVRange groupMemberId subMode - agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff subMode let cd = CDGroupSnd gInfo Nothing -- TODO [short links] move to profileContactRequest? createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing @@ -1087,7 +1087,7 @@ acceptRelayJoinRequestAsync gInfo' <- liftIO $ updateRelayOwnStatusFromTo db gInfo RSInvited RSAccepted ownerMember' <- getGroupMemberById db cxt user groupMemberId pure (gInfo', ownerMember') - agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff chatV subMode + agentAcceptContactAsync cmdId acId True cReqInvId msg PQSupportOff subMode pure r rejectRelayInvitationAsync @@ -1111,7 +1111,7 @@ rejectRelayInvitationAsync user uclId cxt groupRelayInv invId reqChatVRange init (cmdId, acId) <- prepareAgentAccept user False invId PQSupportOff withStore' $ \db -> createJoiningMemberConnection db user uclId (cmdId, acId) chatV reqChatVRange groupMemberId subMode - agentAcceptContactAsync cmdId acId False invId msg PQSupportOff chatV subMode + agentAcceptContactAsync cmdId acId False invId msg PQSupportOff subMode businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, description, image} groupPreferences = @@ -1155,7 +1155,7 @@ shouldIntroduce m vec mem = introduceMember :: User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM () introduceMember _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active" -introduceMember user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do +introduceMember user gInfo toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo gInfo toMember) msgScope sendIntroductions introduceToMembers where @@ -1163,13 +1163,9 @@ introduceMember user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn updateToMemberVector reMembers updateReMembersVectors reMembers shuffledReMembers <- liftIO $ shuffleMembers reMembers - if toMember `supportsVersion` batchSendVersion - then do - let events = map (memberIntroEvt gInfo) shuffledReMembers - forM_ (L.nonEmpty events) $ \events' -> - sendGroupMemberMessages user gInfo conn events' - else forM_ shuffledReMembers $ \reMember -> - void $ sendDirectMemberMessage conn (memberIntroEvt gInfo reMember) groupId + let events = map (memberIntroEvt gInfo) shuffledReMembers + forM_ (L.nonEmpty events) $ \events' -> + sendGroupMemberMessages user gInfo conn events' updateToMemberVector :: [GroupMember] -> CM () updateToMemberVector reMembers = do let relations = map (\GroupMember {indexInGroup} -> (indexInGroup, (IDReferencedIntroduced, MRIntroduced))) reMembers @@ -1315,20 +1311,19 @@ buildGroupRoster mods = take maxGroupRosterSize $ mapMaybe rosterMember mods sendHistory :: User -> GroupInfo -> GroupMember -> CM () sendHistory _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" -sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just conn} = - when (m `supportsVersion` batchSendVersion) $ do - (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) - (errs', fwdMsgsByItem) <- partitionEithers <$> mapM (tryAllErrors . itemForwardMsgs) items - let errors = map ChatErrorStore errs <> errs' - unless (null errors) $ toView $ CEvtChatErrors errors - -- signed items keep the author's original bytes/signature, unsigned are re-encoded; the welcome message - -- (regular groups only; never channels) is an authored element -- all batch together in order. - let fwdEls = map (uncurry encodeFwdElement) (concat fwdMsgsByItem) - welcomeEl <- welcomeElement - let (batches, dropped) = batchElements maxEncodedMsgLength (fwdEls <> maybe [] (: []) welcomeEl) - when (dropped > 0) $ toView $ CEvtChatErrors [ChatError $ CEInternalError ("sendHistory: dropped " <> show dropped <> " oversized history messages")] - forM_ batches $ \body -> - void $ withAgent $ \a -> sendMessages a [(aConnId conn, PQEncOff, MsgFlags False, VRValue Nothing body)] +sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just conn} = do + (errs, items) <- partitionEithers <$> withStore' (\db -> getGroupHistoryItems db user gInfo m 100) + (errs', fwdMsgsByItem) <- partitionEithers <$> mapM (tryAllErrors . itemForwardMsgs) items + let errors = map ChatErrorStore errs <> errs' + unless (null errors) $ toView $ CEvtChatErrors errors + -- signed items keep the author's original bytes/signature, unsigned are re-encoded; the welcome message + -- (regular groups only; never channels) is an authored element -- all batch together in order. + let fwdEls = map (uncurry encodeFwdElement) (concat fwdMsgsByItem) + welcomeEl <- welcomeElement + let (batches, dropped) = batchElements maxEncodedMsgLength (fwdEls <> maybe [] (: []) welcomeEl) + when (dropped > 0) $ toView $ CEvtChatErrors [ChatError $ CEInternalError ("sendHistory: dropped " <> show dropped <> " oversized history messages")] + forM_ batches $ \body -> + void $ withAgent $ \a -> sendMessages a [(aConnId conn, PQEncOff, MsgFlags False, VRValue Nothing body)] where welcomeElement :: CM (Maybe ByteString) welcomeElement = case descrEvent_ of @@ -1347,10 +1342,9 @@ sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just c -- in channels sendHistory runs on the relay, which cannot author XMsgNew (GRRelay < GRObserver); -- the welcome message reaches new members via the channel link data instead | useRelays' gInfo = Nothing - | m `supportsVersion` groupHistoryIncludeWelcomeVersion = do + | otherwise = do let GroupInfo {groupProfile = GroupProfile {description}} = gInfo fmap (\descr -> XMsgNew $ mcSimple (MCText descr)) description - | otherwise = Nothing itemForwardMsgs :: (CChatItem 'CTGroup, (Maybe SignedMsg, Maybe GroupMemberId)) -> CM [(GrpMsgForward, VerifiedMsg 'Json)] itemForwardMsgs (cci, (signedMsg_, signedByGMId_)) = case cci of (CChatItem SMDRcv ci@ChatItem {content = CIRcvMsgContent mc, file}) @@ -1465,7 +1459,7 @@ setGroupLinkData nm user gInfo gLink = do (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays linkType = if useRelays' gInfo then CCTChannel else CCTGroup - sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) + sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData) False Nothing) withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () @@ -1553,28 +1547,26 @@ groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {public authOwnerSig = C.sign' rootPrivKey (ownerId <> C.encodePubKey ownerKey) in [OwnerAuth {ownerId, ownerKey, authOwnerSig}] _ -> [] - userLinkData = UserContactLinkData UserContactData {direct, owners, relays, userData} + userLinkData = UserContactLinkData UserContactData {direct, owners, relays, userData, ratchetKeys = Nothing} crClientData = encodeJSON $ CRDataGroup groupLinkId in (userLinkData, crClientData) restoreShortLink' :: ConnShortLink m -> CM (ConnShortLink m) restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) -getShortLinkConnReq' :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) +getShortLinkConnReq' :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m, ConnectionRequestUri m) getShortLinkConnReq' nm user l = do l' <- restoreShortLink' l withAgent $ \a -> getConnShortLink a nm (aUserId user) l' -getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m) +getShortLinkConnReq :: NetworkRequestMode -> User -> ConnShortLink m -> CM (FixedLinkData m, ConnLinkData m, ConnectionRequestUri m) getShortLinkConnReq nm user l = do - (fd, cData) <- getShortLinkConnReq' nm user l + r@(_, cData, _) <- getShortLinkConnReq' nm user l case cData of ContactLinkData _ UserContactData {direct, relays} - | not supported -> throwChatError CEUnsupportedConnReq - where - supported = direct || not (null relays) + | not direct && null relays -> throwChatError CEUnsupportedConnReq _ -> pure () - pure (fd, cData) + pure r encodeShortLinkData :: J.ToJSON a => a -> UserLinkData encodeShortLinkData d = @@ -1714,7 +1706,7 @@ updatePeerChatVRange conn@Connection {connId, connChatVersion = v, peerChatVRang pure conn {connChatVersion = v', peerChatVRange = msgVRange} else pure conn -- TODO v6.0 remove/review: for contacts only version upgrade should trigger enabling PQ support/encryption - if connType == ConnContact && v' >= pqEncryptionCompressionVersion && (pqSupport /= PQSupportOn || pqEncryption /= PQEncOn) + if connType == ConnContact && (pqSupport /= PQSupportOn || pqEncryption /= PQEncOn) then do withStore' $ \db -> updateConnSupportPQ db connId PQSupportOn PQEncOn pure conn' {pqSupport = PQSupportOn, pqEncryption = PQEncOn} @@ -2118,14 +2110,6 @@ deleteSupportChatIfExists db user gInfo m = do sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] sendDirectContactMessages user ct events = do - Connection {connChatVersion = v} <- liftEither $ contactSendConn_ ct - if v >= batchSend2Version - then sendDirectContactMessages' user ct events - else forM (L.toList events) $ \evt -> - (Right . fst <$> sendDirectContactMessage user ct evt) `catchAllErrors` \e -> pure (Left e) - -sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] -sendDirectContactMessages' user ct events = do conn@Connection {connId} <- liftEither $ contactSendConn_ ct let idsEvts = L.map (ConnectionId connId,Nothing,) events msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} @@ -2261,17 +2245,15 @@ batchSndMessagesJSON :: BatchMode -> NonEmpty (Either ChatError SndMessage) -> [ batchSndMessagesJSON mode = batchMessages mode maxEncodedMsgLength . L.toList encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString -encodeConnInfo chatMsgEvent = do - cxt <- chatStoreCxt - encodeConnInfoPQ PQSupportOff (maxVersion (vr cxt)) chatMsgEvent +encodeConnInfo = encodeConnInfoPQ PQSupportOff -encodeConnInfoPQ :: MsgEncodingI e => PQSupport -> VersionChat -> ChatMsgEvent e -> CM ByteString -encodeConnInfoPQ pqSup v chatMsgEvent = do +encodeConnInfoPQ :: MsgEncodingI e => PQSupport -> ChatMsgEvent e -> CM ByteString +encodeConnInfoPQ pqSup chatMsgEvent = do cxt <- chatStoreCxt let info = ChatMessage {chatVRange = vr cxt, msgId = Nothing, chatMsgEvent} case encodeChatMessage maxEncodedInfoLength info of ECMEncoded connInfo -> case pqSup of - PQSupportOn | v >= pqEncryptionCompressionVersion && B.length connInfo > maxCompressedInfoLength -> do + PQSupportOn | B.length connInfo > maxCompressedInfoLength -> do let connInfo' = compressedBatchMsgBody_ connInfo when (B.length connInfo' > maxCompressedInfoLength) $ throwChatError $ CEException "large compressed info" pure connInfo' @@ -2326,7 +2308,7 @@ deliverMessagesB msgReqs = do lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent where connSupportsPQ = \case - Right (Connection {pqSupport = PQSupportOn, connChatVersion = v}, _, _) -> v >= pqEncryptionCompressionVersion + Right (Connection {pqSupport = PQSupportOn}, _, _) -> True _ -> False compressBodies = forME msgReqs $ \(conn, msgFlags, (mbr, msgIds)) -> runExceptT $ do @@ -2477,10 +2459,9 @@ sendGroupProfileUpdate user gInfo scope asGroup members = (Nothing, Just _) -> True _ -> False sendProfileUpdate = do - let members' = filter (`supportsVersion` memberProfileUpdateVersion) members -- shouldSendProfileUpdate excludes incognito membership, so the badge is presented profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile gInfo (membership gInfo) $ fromLocalProfile p - void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate + void $ sendGroupMessage' user gInfo members $ XInfo profileUpdate currentTs <- liftIO getCurrentTime withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs @@ -2499,12 +2480,12 @@ sendGroupSignedMessages_ gInfo@GroupInfo {groupId} recipientMembers signedEvents sndMsgs_ <- lift $ createSndMessages idsEvts recipientMembers' <- liftIO $ shuffleMembers recipientMembers let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} - (toSendSeparate, toSendBatched, toPending, forwarded, _, dups) = - foldr' (addMember recipientMembers') ([], [], [], [], S.empty, 0 :: Int) recipientMembers' + (toSend, toPending, forwarded, _, dups) = + foldr' (addMember recipientMembers') ([], [], [], S.empty, 0 :: Int) recipientMembers' when (dups /= 0) $ logError $ "sendGroupMessages_: " <> tshow dups <> " duplicate members" -- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here -- Deliver to toSend members - let (sendToMemIds, msgReqs) = prepareMsgReqs msgFlags sndMsgs_ toSendSeparate toSendBatched + let (sendToMemIds, msgReqs) = prepareMsgReqs msgFlags sndMsgs_ toSend delivered <- maybe (pure []) (fmap L.toList . deliverMessagesB) $ L.nonEmpty msgReqs when (length delivered /= length sendToMemIds) $ logError "sendGroupMessages_: sendToMemIds and delivered length mismatch" -- Save as pending for toPending members @@ -2524,29 +2505,24 @@ sendGroupSignedMessages_ gInfo@GroupInfo {groupId} recipientMembers signedEvents liftM2 (<>) (shuffle adminMs) (shuffle otherMs) where isAdmin GroupMember {memberRole} = memberRole >= GRAdmin - addMember members m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) = + addMember members m acc@(toSend, pending, forwarded, !mIds, !dups) = case memberSendAction gInfo events members m of Just a - | mId `S.member` mIds -> (toSendSeparate, toSendBatched, pending, forwarded, mIds, dups + 1) + | mId `S.member` mIds -> (toSend, pending, forwarded, mIds, dups + 1) | otherwise -> case a of - MSASend conn -> ((m, conn) : toSendSeparate, toSendBatched, pending, forwarded, mIds', dups) - MSASendBatched conn -> (toSendSeparate, (m, conn) : toSendBatched, pending, forwarded, mIds', dups) - MSAPending -> (toSendSeparate, toSendBatched, m : pending, forwarded, mIds', dups) - MSAForwarded -> (toSendSeparate, toSendBatched, pending, m : forwarded, mIds', dups) + MSASend conn -> ((m, conn) : toSend, pending, forwarded, mIds', dups) + MSAPending -> (toSend, m : pending, forwarded, mIds', dups) + MSAForwarded -> (toSend, pending, m : forwarded, mIds', dups) Nothing -> acc where mId = groupMemberId' m mIds' = S.insert mId mIds - prepareMsgReqs :: MsgFlags -> NonEmpty (Either ChatError SndMessage) -> [(GroupMember, Connection)] -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq]) - prepareMsgReqs msgFlags msgs toSendSeparate toSendBatched = do + prepareMsgReqs :: MsgFlags -> NonEmpty (Either ChatError SndMessage) -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq]) + prepareMsgReqs msgFlags msgs toSend = do let mode = if useRelays' gInfo then BMBinary else BMJson batched_ = batchSndMessagesJSON mode msgs case L.nonEmpty batched_ of - Just batched' -> do - let lenMsgs = length msgs - (memsSep, mreqsSep) = foldMembers lenMsgs sndMessageMBR msgs toSendSeparate - (memsBtch, mreqsBtch) = foldMembers (length batched' + lenMsgs) msgBatchMBR batched' toSendBatched - (memsSep <> memsBtch, mreqsSep <> mreqsBtch) + Just batched' -> foldMembers (length batched' + length msgs) msgBatchMBR batched' toSend Nothing -> ([], []) where foldMembers :: forall a. Int -> (Maybe Int -> Int -> a -> (ValueOrRef MsgBody, [MessageId])) -> NonEmpty (Either ChatError a) -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq]) @@ -2561,8 +2537,6 @@ sendGroupSignedMessages_ gInfo@GroupInfo {groupId} recipientMembers signedEvents addBody mb (i, (memIds, reqs)) = let req = (conn,msgFlags,) . mkMb memIdx_ i <$> mb in (i - 1, (groupMemberId : memIds, req : reqs)) - sndMessageMBR :: Maybe Int -> Int -> SndMessage -> (ValueOrRef MsgBody, [MessageId]) - sndMessageMBR memIdx_ i SndMessage {msgId, msgBody} = (vrValue_ memIdx_ i msgBody, [msgId]) msgBatchMBR :: Maybe Int -> Int -> MsgBatch -> (ValueOrRef MsgBody, [MessageId]) msgBatchMBR memIdx_ i (MsgBatch batchBody sndMsgs) = (vrValue_ memIdx_ i batchBody, map (\SndMessage {msgId} -> msgId) sndMsgs) vrValue_ memIdx_ i v = case memIdx_ of @@ -2583,31 +2557,26 @@ sendGroupSignedMessages_ gInfo@GroupInfo {groupId} recipientMembers signedEvents createPendingMsg db (groupMemberId, msgId) = createPendingGroupMessage db groupMemberId msgId $> Right () -data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded +data MemberSendAction = MSASend Connection | MSAPending | MSAForwarded memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction -memberSendAction gInfo@GroupInfo {membership} events members m@GroupMember {memberRole, memberStatus} +memberSendAction gInfo@GroupInfo {membership} events members m@GroupMember {memberStatus} -- groups with relays require newer version - we don't need to check member version for batching and forwarding support | useRelays' gInfo = if -- if user is chat relay, send to all non chat relay members - | isRelay membership && not (isRelay m) -> MSASendBatched . snd <$> readyMemberConn m + | isRelay membership && not (isRelay m) -> MSASend . snd <$> readyMemberConn m -- if user is not chat relay, send only to chat relays - | not (isRelay membership) && isRelay m -> MSASendBatched . snd <$> readyMemberConn m + | not (isRelay membership) && isRelay m -> MSASend . snd <$> readyMemberConn m | otherwise -> Nothing -- TODO [relays] MSAForwarded to create GSSForwarded snd statuses? | otherwise = case memberConn m of Nothing -> pendingOrForwarded Just conn@Connection {connStatus} | connDisabled conn || connStatus == ConnDeleted || isConnFailed connStatus || memberStatus == GSMemRejected -> Nothing | connInactive conn -> Just MSAPending - | connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn + | connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn) | otherwise -> pendingOrForwarded where - sendBatchedOrSeparate conn - -- admin doesn't support batch forwarding - send messages separately so that admin can forward one by one - | memberRole >= GRAdmin && not (m `supportsVersion` batchSend2Version) = Just (MSASend conn) - -- either member is not admin, or admin supports batched forwarding - | otherwise = Just (MSASendBatched conn) pendingOrForwarded = case memberCategory m of GCUserMember -> Nothing -- shouldn't happen GCInviteeMember -> Just MSAPending @@ -2616,18 +2585,14 @@ memberSendAction gInfo@GroupInfo {membership} events members m@GroupMember {memb GCPostMember -> forwardSupportedOrPending (invitedByGroupMemberId m) where forwardSupportedOrPending invitingMemberId_ - | membersSupport && all isForwardedGroupMsg events = Just MSAForwarded + | hasInvitingMember && all isForwardedGroupMsg events = Just MSAForwarded | any isXGrpMsgForward events = Nothing | otherwise = Just MSAPending where - membersSupport = - m `supportsVersion` groupForwardVersion && invitingMemberSupportsForward - invitingMemberSupportsForward = case invitingMemberId_ of + hasInvitingMember = case invitingMemberId_ of Just invMemberId -> -- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember - case find (\m' -> groupMemberId' m' == invMemberId) members of - Just invitingMember -> invitingMember `supportsVersion` groupForwardVersion - Nothing -> False + any (\m' -> groupMemberId' m' == invMemberId) members Nothing -> False isXGrpMsgForward event = case event of XGrpMsgForward {} -> True @@ -2652,7 +2617,6 @@ sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} c messageMember :: SndMessage -> CM () messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case MSASend conn -> void $ deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId - MSASendBatched conn -> void $ deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId MSAForwarded -> pure () @@ -2674,7 +2638,9 @@ sendPendingGroupMessages user gInfo GroupMember {groupMemberId} conn = do saveDirectRcvMSG :: forall e. MsgEncodingI e => Connection -> MsgMeta -> ChatMessage e -> CM (Connection, RcvMessage) saveDirectRcvMSG conn@Connection {connId} agentMsgMeta chatMsg@ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do - conn' <- updatePeerChatVRange conn chatVRange + conn' <- case encoding @e of + SJson -> updatePeerChatVRange conn chatVRange + SBinary -> pure conn let agentMsgId = fst $ recipient agentMsgMeta brokerTs = metaBrokerTs agentMsgMeta newMsg = NewRcvMessage {chatMsgEvent, verifiedMsg = VMUnsigned chatMsg, brokerTs} @@ -2853,7 +2819,7 @@ msgContentHasLink mc ft_ = case msgContentTag mc of MCLink_ -> True _ -> maybe False hasLinks ft_ -prepareAgentCreation :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> CM (CommandId, ConnId) +prepareAgentCreation :: User -> CommandFunction -> Bool -> SConnectionMode c -> CM (CommandId, ConnId) prepareAgentCreation user cmdFunction enableNtfs cMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction connId <- withAgent $ \a -> prepareConnectionToCreate a (aUserId user) enableNtfs cMode PQSupportOff @@ -2867,14 +2833,14 @@ prepareAgentJoin user conn_ enableNtfs cReqUri = do Nothing -> withAgent $ \a -> prepareConnectionToJoin a (aUserId user) enableNtfs cReqUri PQSupportOff pure (cmdId, connId) -joinAgentConnectionAsync :: ConnectionModeI c => CommandId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM () +joinAgentConnectionAsync :: CommandId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> CM () joinAgentConnectionAsync cmdId updateConn connId enableNtfs cReqUri cInfo subMode = withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) updateConn connId enableNtfs cReqUri cInfo PQSupportOff subMode allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM () -allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do +allowAgentConnectionAsync user conn@Connection {connId, pqSupport} confId msg = do cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn - dm <- encodeConnInfoPQ pqSupport connChatVersion msg + dm <- encodeConnInfoPQ pqSupport msg withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm withStore' $ \db -> updateConnectionStatus db conn ConnAccepted @@ -2884,9 +2850,9 @@ prepareAgentAccept user enableNtfs invId pqSup = do connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) enableNtfs invId pqSup pure (cmdId, connId) -agentAcceptContactAsync :: MsgEncodingI e => CommandId -> ConnId -> Bool -> InvitationId -> ChatMsgEvent e -> PQSupport -> VersionChat -> SubscriptionMode -> CM () -agentAcceptContactAsync cmdId connId enableNtfs invId msg pqSup chatV subMode = do - dm <- encodeConnInfoPQ pqSup chatV msg +agentAcceptContactAsync :: MsgEncodingI e => CommandId -> ConnId -> Bool -> InvitationId -> ChatMsgEvent e -> PQSupport -> SubscriptionMode -> CM () +agentAcceptContactAsync cmdId connId enableNtfs invId msg pqSup subMode = do + dm <- encodeConnInfoPQ pqSup msg withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) connId enableNtfs invId dm pqSup subMode deleteAgentConnectionAsync :: ConnId -> CM () @@ -2971,9 +2937,11 @@ agentXFTPDeleteSndFilesRemote user sndFiles = do connRequestPQEncryption :: ConnectionRequestUri c -> Maybe PQEncryption connRequestPQEncryption = \case - CRContactUri _ -> Nothing - CRInvitationUri _ (CR.E2ERatchetParamsUri vr' _ _ pq) -> - Just $ PQEncryption $ maxVersion vr' >= CR.pqRatchetE2EEncryptVersion && isJust pq + CRContactUri _ rks -> pqEnc . snd <$> rks + CRInvitationUri _ e2e -> Just $ pqEnc e2e + where + pqEnc (CR.E2ERatchetParamsUri vr' _ _ pq) = + PQEncryption $ maxVersion vr' >= CR.pqRatchetE2EEncryptVersion && isJust pq createRcvFeatureItems :: User -> Contact -> Contact -> CM' () createRcvFeatureItems user ct ct' = @@ -3227,7 +3195,7 @@ adminContactReq = either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" contactCReqHash :: ConnReqContact -> ConnReqUriHash -contactCReqHash = ConnReqUriHash . C.sha256Hash . strEncode +contactCReqHash (CRContactUri crData _) = ConnReqUriHash . C.sha256Hash . strEncode $ CRContactUri crData Nothing simplexChatImage :: ImageData simplexChatImage = ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8KCwkMEQ8SEhEPERATFhwXExQaFRARGCEYGhwdHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAETARMDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD7LooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiivP/iF4yFvv0rSpAZek0yn7v+yPeunC4WpiqihBf8A8rOc5w2UYZ4jEPTourfZDvH3jL7MW03SpR53SWUfw+w96veA/F0erRLY3zKl6owD2k/8Ar15EWLEljknqadDK8MqyxMUdTlWB5Br66WS0Hh/ZLfv1ufiNLj7Mo5m8ZJ3g9OTpy+Xn5/pofRdFcd4B8XR6tEthfMEvVHyk9JB/jXY18fiMPUw9R06i1P3PK80w2aYaOIw8rxf3p9n5hRRRWB6AUUVDe3UFlavc3MixxIMsxppNuyJnOMIuUnZIL26gsrV7m5kWOJBlmNeU+I/Gd9e6sk1hI8FvA2Y1z973NVPGnimfXLoxRFo7JD8if3vc1zefevr8syiNKPtKyvJ9Ox+F8Ycb1cdU+rYCTjTi/iWjk1+nbue3eEPEdtrtoMER3SD95Hn9R7Vu18+6bf3On3kd1aSmOVDkEd/Y17J4P8SW2vWY6R3aD97F/Ue1eVmmVPDP2lP4fyPtODeMoZrBYXFO1Zf+Tf8AB7r5o3qKKK8Q/QgooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAqavbTXmmz20Fw1vJIhVZB1FeDa3p15pWoSWl6hWQHr2YeoNfQlY3izw9Z6/YGGZQky8xSgcqf8K9jKcyWEnyzXuv8D4njLhZ51RVSi7VYLRdGu3k+z+88HzRuq1rWmXmkX8lnexFHU8Hsw9RVLNfcxlGcVKLumfgFahUozdOorSWjT6E0M0kMqyxOyOpyrKcEGvXPAPjCPVolsb9wl6owGPAkH+NeO5p8M0kMqyxOyOpyrA4INcWPy+njKfLLfoz2+HuIMTkmI9pT1i/ij0a/wA+zPpGiuM+H/jCPV4lsL91S+QfKTwJR/jXW3t1BZWslzcyLHFGMsxNfB4jC1aFX2U1r+fof0Rl2bYXMMKsVRl7vXy7p9rBfXVvZWr3NzKscSDLMTXjnjbxVPrtyYoiY7JD8if3vc0zxv4ruNeujFEWjsoz8if3vc1zOa+synKFh0qtVe9+X/BPxvjLjKWZSeEwjtSW7/m/4H5kmaM1HmlB54r3bH51YkzXo3wz8MXMc0es3ZeED/VR5wW9z7VB8O/BpnMerarEREDuhhb+L3Pt7V6cAAAAAAOgFfL5xmqs6FH5v9D9a4H4MlzQzHGq1tYR/KT/AEXzCiiivlj9hCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxfFvh208QWBhmASdRmKUdVP+FeH63pl5pGoSWV5EUdTwezD1HtX0VWL4t8O2fiHTzBONk6g+TKByp/wr28pzZ4WXs6msH+B8NxdwhTzeDxGHVqy/8m8n59n954FmjNW9b0y80fUHsr2MpIp4PZh6iqWfevuYyjOKlF3TPwetQnRm6dRWktGmSwzSQyrLE7I6nKsDgg1teIPFOqa3a29vdy4jiUAheN7f3jWBmjNROhTnJTkrtbGtLF4ijSnRpzajPddHbuP3e9Lmo80ua0scth+a9E+HXgw3Hl6tqsZEX3oYmH3vc+1J8OPBZnKavq0eIhzDCw+9/tH29q9SAAAAGAOgr5bOM35b0KD16v8ARH6twXwXz8uPx0dN4xfXzf6IFAUAAAAdBRRRXyZ+wBRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFB4GTXyj+1p+0ONJjufA3ga6DX7qU1DUY24gB4McZH8Xqe38tqFCdefLETaSufQ3h/4geEde8Uah4a0rWra51Ow/wBfCrD8ceuO+OldRX5I+GfEWseG/ENvr2j30ttqFvJ5iSqxyT3z6g96/RH9nD41aT8U9AWGcx2fiK1QC7tC33/+mieqn07V14zL3QXNHVEQnc9dooorzjQKKKKACiis7xHrel+HdGudY1m8is7K2QvLLI2AAP600m3ZAYfxUg8Pr4VutT1+7isYbSMuLp/4Pb3z6V8++HNd0zxDpq6hpVys8DHGRwVPoR2NeIftJ/G7VPifrbWVk8lp4btZD9mtwcGU/wDPR/c9h2rgfh34z1LwdrAurV2ktZCBcW5PyyD/AB9DX2WTyqYWny1Ho+nY+C4t4Wp5tF16CtVX/k3k/Ps/vPr/ADRmsjwx4g07xFpMWpaZOJInHI/iQ9wR61qbq+mVmro/D6tCdGbp1FZrdEma6/4XafpWoa7jUpV3oA0MLdJD/ntXG5p8E0kMqyxOyOhyrKcEGsMTRlWpShGVm+p1ZbiYYPFQr1IKai72fU+nFAUAKAAOABRXEfDnxpFrMK6fqDhL9BhSeko9frXb1+a4rDVMNUdOotT+k8szLD5lh44jDu8X968n5hRRRXOegFFFFABUGoXlvYWkl1dSrHFGMliaL+7t7C0kuruVYoYxlmNeI+OvFtx4huzHFuisYz+7jz97/aNenluW1MbU00it2fM8S8SUMkoXetR/DH9X5fmeteF/E+m+IFkFoxSWMnMb9cev0rbr5t0vULrTb6K8s5TFNGcgj+R9q9w8E+KbXxDYjlY7xB+9i/qPaurNsneE/eUtYfkeTwlxjHNV9XxVo1V90vTz8vmjoqKKK8I+8CiiigAooooAKKKKACiiigD5V/a8+P0mgvdeAvCUskepFdl9eDjyQR9xPfHeviiR3lkaSR2d2OWZjkk+tfoj+058CtP+Jektq2jxRWnie2T91KMKLlR/yzf+h7V+fOuaVqGiarcaXqtpLaXls5jlikXDKRX0mWSpOlaG/U56l76lKtPwtr+reGNetdb0S8ls761cPHJG2D9D6g9MVmUV6TSasyD9Jf2cfjXpPxR0MW9w0dp4gtkAubYnHmf7aeo/lXr1fkh4W1/V/DGuW2taHey2d9bOHjkjP6H1HtX6Jfs5fGvR/inoQgmeOz8RWqD7XaE439vMT1U+navnMfgHRfPD4fyN4Tvoz12iis7xJremeHdEutZ1i7jtLK1jLyyucAAf1rzUm3ZGgeJNb0vw7otzrOs3kVpZWyF5ZZDgAD+Z9q/PL9pP436r8UNZaxs2ks/Dlq5+z24ODMf77+p9B2o/aU+N2p/FDXDZ2LS2fhy1ci3t84Mx/wCej+/oO1eNV9DgMAqS55/F+RhOd9EFFFABJwBkmvUMzqPh34y1Lwjq63FszSWshAntyeHHt719Z2EstzpVlqD2txbR3kCzxLPGUbawyODXK/slfs8nUpbXx144tGFkhElhp8q4849pHB/h9B3r608X+GLDxBpX2WRFiljX9xIowUPYfT2rGnnkMPWVJ6x6vt/XU+P4o4SjmtN4igrVV/5N5Pz7P7z56zRmrmvaVe6LqMljexMkiHg9mHqKoZr6uEozipRd0z8Rq0J0ZunUVmtGmTwTSQTJNC7JIhyrKcEGvZvhz41j1mJdP1GRUv0GFY8CX/69eJZqSCaWCVZYXZHU5VlOCDXDmGXU8bT5ZaPo+x7WQZ9iMlxHtKesX8UejX+fZn1FRXDfDbxtHrUKadqDqmoIuAx4EoHf613NfnWKwtTC1HTqKzR/QGW5lh8yw8cRh3eL+9Ps/MKr6heW1hZyXd3KsUUYyzGjUby20+zku7yZYoY13MzGvDPHvi+48RXpjiZorCM/u4/73+0feuvLMsqY6pZaRW7/AK6nlcScR0MloXetR/DH9X5D/Hni648Q3nlxlo7GM/u48/e9zXL7qZmjNfodDDwoU1TpqyR+AY7G18dXlXryvJ/19w/dVvSdRutMvo7yzlaOVDkY7+xqkDmvTPhn4HMxj1jV4v3Y+aCFh97/AGjWGPxNHDUXKrt27+R15JlWLzHFxp4XSS1v/L53PQ/C+oXGqaJb3t1bNbyyLkoe/v8AQ1p0AAAAAADoBRX5nUkpSbirLsf0lh6c6dKMJy5mkrvv5hRRRUGwUUUUAFFFFABRRRQAV4d+038CdO+JWkyavo8cdp4mtkzHIBhbkD+B/f0Ne40VpSqypSUovUTV9GfkTruk6joer3Ok6taS2d7ayGOaGVdrKRVKv0T/AGnfgXp/xK0h9Y0iOO18TWqZikAwLkD+B/6Gvz51zStQ0TVbjS9UtZbW8tnKSxSLgqRX1GExccRG636o55RcSlWp4V1/VvDGvWut6JeSWl9bOGjkQ4/A+oPpWXRXU0mrMk/RP4LftDeFvF3ge41HxDfW+lappkG+/idsBwP40HfJ7V8o/tJ/G/VPifrbWVk8tn4btn/0e2zgykfxv6n0HavGwSM4JGeuO9JXFRwFKlUc18vIpzbVgoooAJIAGSa7SQr6x/ZM/Z4k1J7Xxz44tClkMSWFhIuDL3Ejg/w+g70fsmfs8NqMtt448c2eLJCJLCwlX/WnqHcH+H0HevtFFVECIoVVGAAMACvFx+PtenTfqzWEOrEjRI41jjUIigBVAwAPSnUUV4ZsYXjLwzZeJNOaCcBLhQfJmA5U/wCFeBa/pV7ompSWF9GUkToccMOxHtX01WF4z8M2XiXTTBOAk6AmGYDlD/hXvZPnEsHL2dTWD/A+K4r4UhmsHXoK1Zf+TeT8+z+8+c80Zq5r2k3ui6jJY30ZSRTwezD1FUM1+gQlGcVKLumfiFWjOjN06is1umTwTSQTJNE7JIh3KynBBr2PwL8QrO701odbnSC5t0yZCcCUD+teK5pd1cWPy2ljoctTdbPqetkme4rJ6rqUHdPdPZ/8Mdb4/wDGFz4ivDFGxisIz+7j/ve5rls1HuozXTQw1PD01TpqyR5+OxlfHV5V68ryf9fcSZozTAa9P+GHgQzmPWdZhIjHzQQMPvf7R9qxxuMpYOk6lR/8E6MpyfEZriFQoL1fRLux/wAMvApmMesazFiP70EDfxf7R9vavWFAUAAAAcACgAAAAAAdBRX5xjsdVxtXnn8l2P3/ACXJcNlGHVGivV9W/wCugUUUVxHrhRRRQAUUUUAFFFFABRRRQAUUUUAFeH/tOfArT/iXpUmsaSsVp4mto/3UuMLcgDhH/oe1e4Vn+I9a0zw7otzrGsXkVpZWyF5ZZGwAB/WtaNSdOalDcTSa1PyZ1zStQ0TVrnStVtZLS8tnMcsUgwVIqlXp/wC0l8S7T4nePn1aw0q3srO3XyYJBGBNOoPDSHv7DtXmFfXU5SlBOSszlYUUUVYAAScDk19Zfsmfs7vqLW3jjx1ZFLMESafYSjmXuJHHZfQd6+VtLvJtO1K2v7cRtLbyrKgkQOpKnIyp4I46Gv0b/Zv+NOjfFDw+lrIIrDX7RAtzZ8AMMffj9V9u1efmVSrCn7m3Vl00m9T16NEjjWONVRFGFUDAA9KWiivmToCiiigAooooAwfGnhiy8S6cYJwEuEH7mYDlT/hXz7r+k32h6lJYahFskQ8Hsw9QfSvpjUr2106ykvLyZYYYxlmY18+/EXxa/ijU1aOMRWkGRCCBuPuT/Svr+GK2KcnTSvT/ACfl/kfmPiBhMvUI1m7Vn0XVefp0fy9Oa3UbqZmjNfa2PynlJM+9AOajzTo5GjkV0YqynIPoaVg5T1P4XeA/P8vWdaiIj+9BAw+9/tH29q9dAAAAAAHQVwPwx8dQ63Ammai6R6hGuFJ4Ew9vf2rvq/Ms5qYmeJaxGjWy6W8j+gOFcPl9LAReBd0931b8+3oFFFFeSfSBRRRQAUUUUAFFFFABRRRQAUUUUAFFFZ3iTW9L8OaJdazrN5HaWNqheWWQ4AH+NNJt2QB4l1vTPDmiXWs6xdx2llaxl5ZHOAAO3ufavzx/aT+N2qfFDWzZWbSWfhy2ci3tg2DKf77+p9B2pf2lfjdqfxQ1trGxeW08N2z/AOj2+cGYj/lo/v6DtXjVfQ4DAKkuefxfkYTnfRBRRQAScAZNeoZhRXv3w2/Zh8V+Lfh7deJprgadcvHv02zlT5rgdcsf4Qe1eHa5pWoaJq1zpWq2ktpeW0hjlikXDKwrOFanUk4xd2htNFKtTwrr+reGNdtta0S8ltL22cPHIhx07H1HtWXRWjSasxH6S/s4/GrSfijoYtp3jtfENqg+1WpON4/vp6j27V69X5IeFfEGr+F9etdc0O9ks7+1cPHKh/QjuD3Ffoj+zl8bNI+KWhLbztFZ+IraMfa7TON+Osieqn07V85j8A6L54fD+RvCd9GevUUUV5hoFVtTvrXTbGW9vJligiXczNRqd9aabYy3t7MsMEQyzMa+ffiN42uvE96YoS0OmxH91F3b/ab3r1spympmFSy0it3+i8z57iDiCjlFG71qPZfq/Id8RPGl14lvTFEzRafGf3cf97/aNclmmZozX6Xh8NTw1NU6askfheNxdbG1pV68ryY/NGTTM16R4J+GVxrGkSX+pSSWfmJ/oq45J7MR6Vni8ZRwkOes7I1y7K8TmNX2WHjd7/0zzvJozV3xDpF7oepyWF/EUkQ8HHDD1FZ+feuiEozipRd0zjq0Z0puE1ZrdE0E8sEyTQu0ciHKspwQa9z+GHjuLXIU0zUpFTUEXCseBKB/WvBs1JBPLBMk0LmORCGVlOCDXn5lllLH0uWWjWz7HsZFnlfJ6/tKesXuu6/z7M+tKK4D4X+PItdhTTNSdY9SQYVicCYDuPf2rv6/M8XhKuEqulVVmj92y7MaGYUFXoO6f4Ps/MKKKK5juCiiigAooooAKKKKACiig9KAM7xLrmleG9EudZ1q8jtLG2QvLK5wAPQep9q/PH9pP43ap8T9beyspJbTw3bSH7NbZx5pH8b+p9u1bH7YPxL8XeJPG114V1G0udH0jT5SIrNuDOR0kbs2e3pXgdfRZfgVTSqT3/IwnO+iCiigAkgAZJr1DMK+s/2TP2d31Brbxz46tNtmMSafp8i8y9/MkB6L0wO9J+yb+zwdSe28b+ObLFmpEljYSr/rT1DuP7voO9faCKqIERQqqMAAYAFeLj8fa9Om/VmsIdWEaJGixooVFGFUDAA9K8Q/ac+BWnfErSZNY0mOO08T2yZilAwtyAPuP/Q9q9worx6VWVKSlF6mrSasfkTrmlahomrXOlaray2l7bSGOaKRcMrCqVfon+098C7D4l6U+s6Skdr4mtY/3UmMC5UdI29/Q1+fOt6XqGi6rcaVqlrJa3ls5SWKQYKkV9RhMXHERut+qOeUeUpVqeFfEGreGNdttb0W7ktb22cNG6HH4H1FZdFdTSasyT9Jf2cPjVpXxR0Fbe4eK18Q2qD7Va7sbx/z0T1H8q9V1O+tdNsZb29mWGCJdzMxr8ovAOoeIdK8W2GoeF5podVhlDQtEefcH2PevsbxP4417xTp1jDq3lQGKFPOigJ2NLj5m59849K4KHD0sTX9x2h18vJHj55xDSyqhd61Hsv1fkaXxG8bXXie9MURaLTo2/dR5+9/tH3rkM1HmjNffYfC08NTVOmrJH4ljMXWxtaVau7yZJmgHmmAmvWfhN8PTceVrmuQkRDDW9uw+9/tN7Vjj8dSwNJ1ar9F3OjK8pr5nXVGivV9Eu7H/Cf4emcx63rkJEfDW9u4+9/tMPT2r2RQFAVQABwAKAAAAAAB0Aor8uzDMKuOq+0qfJdj9zyjKMPlVBUaK9X1bOf8b+FbHxRppt7gCO4UfuZwOUP9R7V86+IdHv8AQtTk0/UIikqHg9mHqD6V9VVz3jnwrY+KNMNvcKEuEBME2OUP+FenkmdywUvZVdab/A8PijheGZw9vQVqq/8AJvJ+fZnzLuo3Ve8Q6Pf6FqclhqERjkQ8Hsw9Qazs1+jwlGpFSi7pn4xVozpTcJqzW6J7eeSCZJoZGjkQhlZTgg17t8LvHsWuQppmpOseooMKxPEw/wAa8DzV3Q7fULvVIIdLWQ3ZcGMx8EH1z2rzs1y2jjaLVTRrZ9v+AezkGcYnK8SpUVzKWjj3/wCD2PrCiqOgx38Oj20eqTJNeLGBK6jAJq9X5VOPLJq9z98pyc4KTVr9H0CiiipLCiiigAooooAKKKKAPK/2hfg3o/xT8PFdsVprlupNnebec/3W9VNfnR4y8Naz4R8RXWg69ZvaXts5V1YcEdmB7g9jX6115V+0P8GtF+Knh05SO0161UmzvQuD/uP6qf0r08DjnRfJP4fyM5wvqj80RycCvrP9kz9ndtRNr458dWTLaAiTT9PlXBl9JJB/d7gd+tXv2bv2Y7yz19vEHxFs1VbKYi1sCQwlZTw7f7PcDvX2CiLGioihVUYAAwAK6cfmGns6T9WTCHVhGiRoqRqFRRgKBgAUtFFeGbBRRRQAV4h+038CtP8AiZpTatpCQ2fia2jPlS4wtyo52P8A0Pavb6K0pVZUpKUXqJq+jPyJ1zStQ0TVrnStVtJbS9tnMcsUgwVIqPS7C61O+isrKFpZ5W2qor9AP2r/AIM6J448OzeJLV7fTtesoyRO3yrcqP4H9/Q14F8OvBlp4XsvMkCTajKP3suM7f8AZX0H86+1yiDzFcy0S3Pms+zqllNLXWb2X6vyH/DnwZaeF7EPIEm1CUDzZcfd/wBke1dfmo80ua+0pUY0oqMVofjWLxNXF1XWrO8mSZozUea9N+B/hTTdau5NUv5opvsrjbak8k9mYelc+OxcMHQlWqbI1y3LqmYYmOHpbvuafwj+HhnMWva5DiMENb27D73ozD09q9oAAAAAAHQCkUBVCqAAOABS1+U5jmNXH1XUqfJdj9yyjKKGV0FRor1fVsKKKK4D1AooooA57xz4UsPFOmG3uFEdwgJgnA5Q/wBR7V84eI9Gv9A1SXT9RhMcqHg/wuOxB7ivrCud8d+E7DxTpZt51CXKDMEwHKn/AAr6LI88lgpeyq603+Hmv1Pj+J+GIZnB16KtVX/k3k/Psz5p0uxu9Tv4rGxheaeVtqIoyTX0T8OPBNp4XsRJKFm1GQfvZf7v+yvtR8OfBFn4UtDIxW41CUfvJsdB/dX0FdfWue568W3RoP3Pz/4BhwvwtHL0sTiVeq9l/L/wQooor5g+3CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKrarf2ml2E19fTpBbwrud2OAKTVdQtNLsJb6+mWGCJcszGvm34nePLzxXfmGEtDpkTfuos/f/wBpvevZyfJ6uZVbLSC3f6LzPBz3PaOVUbvWb2X6vyH/ABM8d3fiq/MULPDpsR/dRdN3+03vXF5pm6jdX6phsLTw1JUqSskfjGLxVbGVnWrO8mSZ96M0wGnSq8UhjkRkdeCrDBFb2OXlFzWn4b1y/wBA1SPUNPmMciHkdmHoR6Vk7hS596ipTjUi4zV0y6c50pqcHZrZn1X4C8W2HizShc27BLmMATwZ5Q/4V0dfIfhvXL/w/qseo6dMY5U6js47gj0r6Y8BeLtP8WaUtzbER3KAefATyh/qPevzPPshlgJe1pa03+Hk/wBGfr/DfEkcygqNbSqv/JvNefdHSUUUV80fWhRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFVtVv7TS7CW+vp1ht4l3O7HpSatqNnpWny319OsMES7mZjXzP8UfH154tv8AyYWeDS4WPlQ5xvP95vU/yr2smyarmVWy0gt3+i8zws8zylldK71m9l+r8h/xP8eXfiy/MUJaHTIm/cxZ5b/ab3ris0zNGa/V8NhaWFpKlSVkj8bxeKrYuq61Z3kx+aX2pmTXsnwc+GrXBh8Qa/CViB3W9sw5b0Zh6e1YZhj6OAourVfourfY3y3LK+Y11Ror1fRLux3wc+GxuPK1/X4SIgQ1tbuPvf7TD09BXT/Fv4dQ6/bPqukxpFqca5KgYE4Hb6+9ekKAqhVAAHAApa/L62fYupi1ilKzWy6W7f5n63R4bwVPBPBuN0931v3/AMj4wuIZred4J42jlQlWVhgg0zNfRHxc+HUXiCB9W0mNI9TRcso4EwH9a+eLiKW2neCeNo5UO1kYYIPpX6TlOa0cypc8NJLddv8AgH5XnOS1srrck9YvZ9/+CJmtPw1rl/4f1WLUdPmMcqHkZ4Yeh9qys0Zr0qlONSLhNXTPKpznSmpwdmtmfWHgDxfp/i3SVubZhHcoAJ4CfmQ/1HvXSV8feGdd1Dw9q0WpabMY5UPIz8rr3UjuK+nPAHjDT/FulLcW7CO6QYngJ5Q/1FfmGfZBLAS9rS1pv8PJ/oz9c4c4jjmMFRraVV/5N5rz7o6WiiivmT6wKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAOY+JXhRfFvh5rAXDwTod8LA/KW9GHcV8s65pV/oupzadqNu0FxC2GVu/uPUV9m1x/xM8DWHi/TD8qw6jEP3E4HP+6fUV9Tw7n7wEvY1v4b/AAf+Xc+S4k4eWYR9vR/iL8V29ex8q5o+gq9ruk32i6nLp2oQNFPG2CCOvuPUV6v8Gvhk1w0PiDxDBiH71tbOPvejMPT2r9Cx2Z4fB4f283o9rdfQ/OMBlWIxuI+rwjZre/T1F+DPw0NwYfEPiCDEQ+a2tnH3vRmHp6Cvc1AVQqgADgAUKoVQqgAAYAHalr8lzPMq2Y1nVqv0XRI/YsryuhltBUqS9X1bCiiivOPSCvNfi98OYvEVu+raTEseqRrllHAnHoff3r0qiuvBY2tgqyq0nZr8fJnHjsDRx1F0ayun+Hmj4ruIZbad4J42ilQlWRhgg1Hmvoz4vfDiLxDA+raRGseqRjLIOBOP8a8AsdI1K91hdIgtJDetJ5ZiK4Knvn0xX6zleb0Mwoe1Ts1uu3/A8z8dzbJK+XYj2TV0/hff/g+Q3SbC81XUIbCwgee4mYKiKOpr6a+F3ga28IaaWkYTajOo8+Tsv+yvtTPhd4DtPCWnCWULNqcq/vZcfd/2V9q7avh+IeIHjG6FB/u1u+//AAD73hrhuOBSxGIV6j2X8v8AwQooor5M+xCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAxdd8LaHrd/a32pWKTT2rbo2Pf2PqK2VAVQqgAAYAHalorSVWc4qMm2lt5GcKNOEnKMUm9/MKKKKzNAooooAKKKKACs+HRdLh1iXV4rKFb6VQrzBfmIrQoqozlG/K7XJlCMrOSvYKKKKkoKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooA//2Q==" diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index a65bed75a0..b3f3a0fbe6 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -107,6 +107,7 @@ import Text.Read (readMaybe) import UnliftIO.Concurrent (ThreadId, forkIO, mkWeakThreadId) import UnliftIO.Directory import UnliftIO.STM +import qualified Data.Aeson as J smallGroupsRcptsMemLimit :: Int smallGroupsRcptsMemLimit = 20 @@ -127,6 +128,8 @@ processAgentMessage _ _ (DEL_CONNS connIds) = toView $ CEvtAgentConnsDeleted $ L.map AgentConnId connIds processAgentMessage _ "" (ERR e) = eToView $ chatErrorAgent e +processAgentMessage _ connId (SSENT _ _) = + toView $ CEvtServiceReplySent (AgentConnId connId) processAgentMessage corrId connId msg = do lockEntity <- critical connId (withStore (`getChatLockEntity` AgentConnId connId)) withEntityLock "processAgentMessage" lockEntity $ do @@ -439,7 +442,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change" processDirectMessage :: AEvent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM () - processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case + processDirectMessage agentMsg connEntity conn@Connection {connId, viaUserContactLink, customUserProfileId, connectionCode} = \case Nothing -> case agentMsg of CONF confId pqSupport _ connInfo -> do chatRelayTests_ <- asks chatRelayTests @@ -515,7 +518,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = withCompletedCommand conn agentMsg $ \_ -> case cReq of CRInvitationUri _ _ -> withStore' $ \db -> setConnConnReqInv db user connId cReq - CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" + CRContactUri _ _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" + RJCT reason -> do + ct' <- withStore' $ \db -> updateContactStatus db user ct CSRejected + toView $ CEvtContactRequestRejected user ct' (either (const Nothing) Just $ strDecode reason) MSG msgMeta _msgFlags msgBody -> do tags <- newTVarIO [] withAckMessage "contact msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do @@ -642,19 +648,6 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when (contactConnInitiated conn') $ do probeMatchingMembers ct' (contactConnIncognito ct') withStore' $ \db -> resetContactConnInitiated db user conn' - forM_ viaUserContactLink $ \userContactLinkId -> do - (ucl, gli_) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - -- let UserContactLink {addressSettings = AddressSettings {autoReply}} = ucl - when (connChatVersion < batchSend2Version) $ forM_ (autoReply $ addressSettings ucl) $ \mc -> sendAutoReply ct' mc Nothing -- old versions only - -- TODO REMOVE LEGACY vvv - forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do - groupInfo <- withStore $ \db -> getGroupInfo db cxt user groupId - subMode <- chatReadVar subscriptionMode - groupConnIds@(cmdId, grpConnId) <- prepareAgentCreation user CFCreateConnGrpInv True SCMInvitation - gVar <- asks random - withStore $ \db -> createNewContactMemberAsync db gVar user groupInfo ct' gLinkMemRole groupConnIds connChatVersion peerChatVRange subMode - withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) grpConnId True SCMInvitation CR.IKPQOff subMode - -- TODO REMOVE LEGACY ^^^ SENT msgId proxy -> do void $ continueSending connEntity conn sentMsgDeliveryEvent conn msgId @@ -705,7 +698,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> when (directOrUsed ct && sqSecured) $ do toView $ CEvtContactSndReady user ct - when (connChatVersion >= batchSend2Version) $ forM_ viaUserContactLink $ \userContactLinkId -> do + forM_ viaUserContactLink $ \userContactLinkId -> do (ucl, _) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId forM_ (autoReply $ addressSettings ucl) $ \mc -> do connReq_ <- pure (contactRequestId' ct) $>>= \connReqId -> withStore' (\db -> getContactRequest' db user connReqId) @@ -740,55 +733,20 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () - processGroupMessage agentMsg connEntity conn@Connection {connId, connChatVersion, customUserProfileId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of + processGroupMessage agentMsg connEntity conn@Connection {connId, customUserProfileId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of INV (ACR _ cReq) -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cReq of groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of -- [async agent commands] XGrpMemIntro continuation on receiving INV - CFCreateConnGrpMemInv - | maxVersion (peerChatVRange conn) >= groupDirectInvVersion -> sendWithoutDirectCReq - | otherwise -> messageError "processGroupMessage INV: member chat version range incompatible" - where - sendWithoutDirectCReq = do - let GroupMember {groupMemberId, memberId} = m - hostConnId <- withStore $ \db -> do - liftIO $ setConnConnReqInv db user connId cReq - getHostConnId db user groupId - sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} - -- TODO REMOVE LEGACY vvv - -- [async agent commands] group link auto-accept continuation on receiving INV - CFCreateConnGrpInv -> do - (ct, groupLinkId) <- withStore $ \db -> do - ct <- getContactViaMember db cxt user m - liftIO $ setNewContactMemberConnRequest db user m cReq - liftIO $ (ct,) <$> getGroupLinkId db user gInfo - if memberRole' membership >= GRAdmin - then do - sendGrpInvitation ct m groupLinkId - toView $ CEvtSentGroupInvitation user gInfo ct m - else messageError "processGroupMessage: group link host no longer has admin role" - where - sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> CM () - sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do - let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo - GroupMember {memberRole = userRole, memberId = userMemberId} = membership - groupInv = - GroupInvitation - { fromMember = MemberIdRole userMemberId userRole, - invitedMember = MemberIdRole memberId memRole, - connRequest = cReq, - groupProfile, - business = Nothing, - groupLinkId = groupLinkId, - groupSize = Just currentMemCount - } - (_msg, _) <- sendDirectContactMessage user ct $ XGrpInv groupInv - -- we could link chat item with sent group invitation message (_msg) - createInternalChatItem user (CDGroupRcv gInfo Nothing m) (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - -- TODO REMOVE LEGACY ^^^ + CFCreateConnGrpMemInv -> do + let GroupMember {groupMemberId, memberId} = m + hostConnId <- withStore $ \db -> do + liftIO $ setConnConnReqInv db user connId cReq + getHostConnId db user groupId + sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} _ -> throwChatError $ CECommandError "unexpected cmdFunction" - CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" + CRContactUri _ _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" CONF confId _pqSupport _ connInfo -> do ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo conn' <- updatePeerChatVRange conn chatVRange @@ -955,7 +913,6 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = toView $ CEvtJoinedGroupMember user gInfo'' m' {memberStatus = mStatus} let Connection {viaUserContactLink} = conn when (isJust viaUserContactLink && isNothing (memberContactId m')) $ sendXGrpLinkMem gInfo'' - when (connChatVersion < batchSend2Version) $ getAutoReplyMsg >>= mapM_ (\mc -> sendGroupAutoReply mc Nothing) if useRelays' gInfo'' then do introduceInChannel cxt user gInfo'' m' @@ -1223,12 +1180,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = JOINED sqSecured -> -- [async agent commands] continuation on receiving JOINED when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> - when (sqSecured && connChatVersion >= batchSend2Version) $ do + when (sqSecured) $ do mc_ <- getAutoReplyMsg forM_ mc_ $ \mc -> do connReq_ <- withStore' $ \db -> getBusinessContactRequest db user groupId sendGroupAutoReply mc connReq_ - LDATA FixedLinkData {linkConnReq = cReq, rootKey = relayKey, linkEntityId} cData -> + LDATA FixedLinkData {rootKey = relayKey, linkEntityId} cData cReq -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cmdFunction of CFGetRelayDataJoin -> do @@ -1240,14 +1197,14 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = pure $ MemberId entityId _ -> throwChatError $ CEException "relay link: no relay link data or entity id" case cReq of - CRContactUri crData@ConnReqUriData {crClientData} -> do + CRContactUri crData@ConnReqUriData {crClientData} e2e -> do let pqSup = PQSupportOff lift (withAgent' $ \a -> connRequestPQSupport a pqSup cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq - Just (agentV, _) -> do - let chatV = agentToChatVersion agentV + Just _ -> do + let chatV = initialChatVersion groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli - cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} + cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} e2e -- Update connection with data derived from cReq, now available after getConnShortLinkAsync withStore' $ \db -> updateConnLinkData db user conn cReq cReqHash groupLinkId chatV pqSup let incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo @@ -1395,16 +1352,24 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = processContactConnMessage :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () processContactConnMessage agentMsg connEntity conn UserContact {userContactLinkId = uclId, groupId = ucGroupId_} = case agentMsg of - REQ invId pqSupport _ connInfo -> do + REQ invId pqSupport _ connInfo rejectionSupported -> do (signedMsg_, ChatMessage {chatVRange, chatMsgEvent}) <- parseChatMessage' conn connInfo case chatMsgEvent of - XContact p xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport + XContact p xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport rejectionSupported XMember p joiningMemberId joiningMemberKey viaRelay -> memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey viaRelay - XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport + XInfo p -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport rejectionSupported XGrpRelayInv groupRelayInv -> xGrpRelayInv invId chatVRange groupRelayInv XGrpRelayTest challenge _ -> xGrpRelayTest invId chatVRange challenge -- TODO show/log error, other events in contact request _ -> pure () + SREQ invId sigKey_ payload -> + chatReadVar processServiceRequests >>= \case + True -> case J.eitherDecodeStrict' payload of + Right request -> toView $ CEvtServiceRequest user (AgentInvId invId) sigKey_ request + Left _ -> dropSReq + False -> dropSReq + where + dropSReq = withAgent $ \a -> rejectServiceRequest a NRMBackground (aUserId user) invId Nothing LINK _link auData -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> case cmdFunction of @@ -1466,8 +1431,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () where - profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> CM () - profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ welcomeMsgId_ requestMsg_ reqPQSup = do + profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> Bool -> CM () + profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ welcomeMsgId_ requestMsg_ reqPQSup rejectionSupported = do (ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId let v = maxVersion chatVRange case gLinkInfo_ of @@ -1477,7 +1442,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = AddressSettings {autoAccept} = addressSettings isSimplexTeam = sameConnReqContact connReq adminContactReq gVar <- asks random - withStore (\db -> createOrUpdateContactRequest db gVar cxt user uclId ucl isSimplexTeam invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ reqPQSup) >>= \case + withStore (\db -> createOrUpdateContactRequest db gVar cxt user uclId ucl isSimplexTeam invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ reqPQSup rejectionSupported) >>= \case RSAcceptedRequest _ucr re -> case re of REContact ct -> -- TODO [short links] update request msg @@ -1618,15 +1583,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = | otherwise -> do acceptMember_ <- asks $ acceptMember . chatHooks . config maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case - Right (acceptance, useRole) - | v < groupFastLinkJoinVersion -> - messageError "processContactConnMessage: chat version range incompatible for accepting group join request" - | otherwise -> do - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing Nothing - (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem - createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' + Right (acceptance, useRole) -> do + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing Nothing + (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' Left rjctReason | v < groupJoinRejectVersion -> messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" @@ -1657,7 +1619,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = withStore $ \db -> do Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode liftIO $ setCommandConnId db user cmdId testCId - agentAcceptContactAsync cmdId acId True invId msg PQSupportOff chatV subMode + agentAcceptContactAsync cmdId acId True invId msg PQSupportOff subMode | otherwise = messageError "relay test sent to non-relay link" where User {userChatRelay} = user @@ -2822,10 +2784,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = maybeCreateGroupDescrLocal :: GroupInfo -> GroupMember -> CM () maybeCreateGroupDescrLocal gInfo@GroupInfo {groupProfile = GroupProfile {description}} m = - unless expectHistory $ forM_ description $ \descr -> + unless (groupFeatureAllowed SGFHistory gInfo) $ forM_ description $ \descr -> createInternalChatItem user (CDGroupRcv gInfo Nothing m) (CIRcvMsgContent $ MCText descr) Nothing - where - expectHistory = groupFeatureAllowed SGFHistory gInfo && m `supportsVersion` groupHistoryIncludeWelcomeVersion processMemberProfileUpdate :: GroupInfo -> GroupMember -> Profile -> Maybe (RcvMessage, UTCTime) -> CM GroupMember processMemberProfileUpdate gInfo m@GroupMember {memberProfile = p, memberContactId} p' msgTs_ @@ -3223,16 +3183,14 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) case memChatVRange of Nothing -> messageError "x.grp.mem.intro: member chat version range incompatible" - Just (ChatVersionRange mcvr) - | maxVersion mcvr >= groupDirectInvVersion -> do - subMode <- chatReadVar subscriptionMode - groupConnIds@(cmdId, connId) <- prepareAgentCreation user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation - let chatV = maybe (minVersion (vr cxt)) (\peerVR -> vr cxt `peerConnChatVersion` fromChatVRange peerVR) memChatVRange - void $ withStore $ \db -> do - reMember <- createIntroReMember db cxt user gInfo memInfo memRestrictions - createIntroReMemberConn db user m reMember chatV memInfo groupConnIds subMode - withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) connId (chatHasNtfs chatSettings) SCMInvitation CR.IKPQOff subMode - | otherwise -> messageError "x.grp.mem.intro: member chat version range incompatible" + Just _ -> do + subMode <- chatReadVar subscriptionMode + groupConnIds@(cmdId, connId) <- prepareAgentCreation user CFCreateConnGrpMemInv (chatHasNtfs chatSettings) SCMInvitation + let chatV = maybe (minVersion (vr cxt)) (\peerVR -> vr cxt `peerConnChatVersion` fromChatVRange peerVR) memChatVRange + void $ withStore $ \db -> do + reMember <- createIntroReMember db cxt user gInfo memInfo memRestrictions + createIntroReMemberConn db user m reMember chatV memInfo groupConnIds subMode + withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) connId (chatHasNtfs chatSettings) SCMInvitation CR.IKPQOff True subMode _ -> messageError "x.grp.mem.intro can be only sent by host member" sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> CM () @@ -4448,7 +4406,7 @@ runRelayRequestWorker a Worker {doWork} = do where getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact) getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} gInfo = do - (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' NRMBackground user reqGroupLink + (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners}), _) <- getShortLinkConnReq' NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data" Just GroupShortLinkData {groupProfile = gp@GroupProfile {publicGroup}} -> do @@ -4478,15 +4436,15 @@ runRelayRequestWorker a Worker {doWork} = do sigKeys <- liftIO $ atomically $ C.generateKeyPair gVar let crClientData = encodeJSON $ CRDataGroup groupLinkId -- prepare link with relayMemId as linkEntityId (no server request) - (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) Nothing + (ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) CR.IKPQOff False Nothing ccLink' <- setShortLinkType CCTGroup <$> shortenCreatedLink ccLink sLnk <- case connShortLink' ccLink' of Just sl -> pure sl Nothing -> throwChatError $ CEException "failed to create relay link: no short link" let userData = encodeShortLinkData $ RelayShortLinkData {relayProfile = fromLocalProfile p} - userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData} + userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData, ratchetKeys = Nothing} -- create connection with prepared link (single network call) - connId <- withAgent $ \a' -> createConnectionForLink a' NRMBackground (aUserId user) True ccLink preparedParams userLinkData CR.IKPQOff subMode + connId <- withAgent $ \a' -> createConnectionForLink a' NRMBackground (aUserId user) True ccLink preparedParams userLinkData subMode -- TODO [relays] starting role should be communicated in protocol from owner to relays subRole <- asks $ channelSubscriberRole . config void $ withFastStore $ \db -> createGroupLink db gVar user gi connId ccLink' groupLinkId subRole subMode diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index e8cd381941..cd2e337aff 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -349,7 +349,7 @@ markdownP = mconcat <$> A.many' fragmentP simplexUriFormat :: Maybe Text -> AConnectionLink -> Format simplexUriFormat showText = \case ACL m (CLFull cReq) -> case cReq of - CRContactUri crData -> SimplexLink showText (linkType' crData) cLink $ uriHosts crData + CRContactUri crData _ -> SimplexLink showText (linkType' crData) cLink $ uriHosts crData CRInvitationUri crData _ -> SimplexLink showText XLInvitation cLink $ uriHosts crData where cLink = ACL m $ CLFull $ simplexConnReqUri cReq diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 4e3dc3ab34..fbf2a226e1 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -276,6 +276,7 @@ mobileChatOpts dbOptions = optFilesFolder = Nothing, optTempDirectory = Nothing, showReactions = False, + showFullLinks = False, allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 1846cb3582..4e47868fa0 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -46,6 +46,7 @@ data ChatOpts = ChatOpts optFilesFolder :: Maybe FilePath, optTempDirectory :: Maybe FilePath, showReactions :: Bool, + showFullLinks :: Bool, allowInstantFiles :: Bool, autoAcceptFileSize :: Integer, muteNotifications :: Bool, @@ -418,6 +419,11 @@ chatOptsP appDir defaultDbName = do ( long "reactions" <> help "Show message reactions" ) + showFullLinks <- + switch + ( long "show-full-links" + <> help "Show full connection links and addresses" + ) allowInstantFiles <- switch ( long "allow-instant-files" @@ -485,6 +491,7 @@ chatOptsP appDir defaultDbName = do optFilesFolder, optTempDirectory, showReactions, + showFullLinks, allowInstantFiles, autoAcceptFileSize, muteNotifications, diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 19ba3d022d..8d9c8ecfcc 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -56,7 +56,6 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import qualified Simplex.FileTransfer.Description as FD -import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder, fromTextField_) import Simplex.Messaging.Compression (Compressed, compress1, decompress1, decompressedSize) import qualified Simplex.Messaging.Crypto as C @@ -98,38 +97,6 @@ supportedChatVRange :: VersionRangeChat supportedChatVRange = mkVersionRange initialChatVersion currentChatVersion {-# INLINE supportedChatVRange #-} --- version range that supports skipping establishing direct connections in a group and establishing direct connection via x.grp.direct.inv -groupDirectInvVersion :: VersionChat -groupDirectInvVersion = VersionChat 2 - --- version range that supports joining group via group link without creating direct contact -groupFastLinkJoinVersion :: VersionChat -groupFastLinkJoinVersion = VersionChat 3 - --- version range that supports group forwarding -groupForwardVersion :: VersionChat -groupForwardVersion = VersionChat 4 - --- version range that supports batch sending in groups -batchSendVersion :: VersionChat -batchSendVersion = VersionChat 5 - --- version range that supports sending group welcome message in group history -groupHistoryIncludeWelcomeVersion :: VersionChat -groupHistoryIncludeWelcomeVersion = VersionChat 6 - --- version range that supports sending member profile updates to groups -memberProfileUpdateVersion :: VersionChat -memberProfileUpdateVersion = VersionChat 7 - --- version range that supports compressing messages and PQ e2e encryption -pqEncryptionCompressionVersion :: VersionChat -pqEncryptionCompressionVersion = VersionChat 8 - --- version range that supports batch sending in direct connections, and forwarding batched messages in groups -batchSend2Version :: VersionChat -batchSend2Version = VersionChat 9 - -- supports differentiating business chats when joining contact addresses businessChatsVersion :: VersionChat businessChatsVersion = VersionChat 10 @@ -167,11 +134,6 @@ relayWebCapVersion = VersionChat 18 groupRosterVersion :: VersionChat groupRosterVersion = VersionChat 19 -agentToChatVersion :: VersionSMPA -> VersionChat -agentToChatVersion v - | v < pqdrSMPAgentVersion = initialChatVersion - | otherwise = pqEncryptionCompressionVersion - data ConnectionEntity = RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact} | RcvGroupMsgConnection {entityConnection :: Connection, groupInfo :: GroupInfo, groupMember :: GroupMember} diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 89100ff890..39405bd1ba 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 5, 0, 12] +minRemoteCtrlVersion = AppVersion [7, 0, 0, 11] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 5, 0, 12] +minRemoteHostVersion = AppVersion [7, 0, 0, 11] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 80c928567e..fe7539fad7 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -113,25 +113,27 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do [sql| SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = c.contact_request_id WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0 |] (userId, contactId, CSActive) toContact' :: UTCTime -> Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact - toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = + toContact' currentTs contactId conn chatTags ((profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) = let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge currentTs badgeRow, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn preparedContact = toPreparedContact preparedContactRow + contactRequest = UserContactRequestRef <$> contactRequestId <*> (unBI <$> rejectionSupported_) groupDirectInv = toGroupDirectInvitation groupDirectInvRow - in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactRequest, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = do currentTs <- liftIO getCurrentTime diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index 75652349b5..146f957947 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -62,6 +62,7 @@ createOrUpdateContactRequest :: Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> + Bool -> ExceptT StoreError IO RequestStage createOrUpdateContactRequest db @@ -77,7 +78,8 @@ createOrUpdateContactRequest xContactId_ welcomeMsgId_ requestMsg_ - pqSup = + pqSup + rejectionSupported = case xContactId_ of -- 0) this is very old legacy, when we didn't have xContactId at all (this should be deprecated) Nothing -> createContactRequest @@ -113,7 +115,7 @@ createOrUpdateContactRequest SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, @@ -124,6 +126,7 @@ createOrUpdateContactRequest FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = ct.contact_request_id WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 |] (userId, xContactId) @@ -147,7 +150,7 @@ createOrUpdateContactRequest [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -177,11 +180,11 @@ createOrUpdateContactRequest [sql| INSERT INTO contact_requests (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, - created_at, updated_at, xcontact_id, welcome_shared_msg_id, request_shared_msg_id, pq_support) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, xcontact_id, welcome_shared_msg_id, request_shared_msg_id, pq_support, rejection_supported) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (uclId, Binary invId, minV, maxV, profileId, ldn, userId) - :. (currentTs, currentTs, xContactId_, welcomeMsgId_, fst <$> requestMsg_, pqSup) + :. (currentTs, currentTs, xContactId_, welcomeMsgId_, fst <$> requestMsg_, pqSup, BI rejectionSupported) ) contactRequestId <- liftIO $ insertedRowId db createRequestEntity ldn profileId contactRequestId currentTs diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 723b12d448..7ddcc08a58 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -322,7 +322,7 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, @@ -334,6 +334,7 @@ getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN connections c ON c.contact_id = ct.contact_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = ct.contact_request_id WHERE ( (c.user_id = ? AND c.via_contact_uri_hash = ?) OR (c.user_id = ? AND c.via_contact_uri_hash = ?) @@ -848,7 +849,7 @@ contactRequestQuery = [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -918,6 +919,7 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, + contactRequest = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, groupDirectInv = Nothing, @@ -971,7 +973,7 @@ getContact_ db cxt user@User {userId} contactId deleted = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, @@ -983,6 +985,7 @@ getContact_ db cxt user@User {userId} contactId deleted = do FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = ct.contact_request_id WHERE ct.user_id = ? AND ct.contact_id = ? AND ct.deleted = ? |] diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index d607541bda..c8fd232e3f 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -27,7 +27,6 @@ module Simplex.Chat.Store.Groups getGroupLinkConnection, deleteGroupLink, getGroupLink, - getGroupLinkId, setGroupLinkMemberRole, setGroupLinkShortLink, createNewGroup, @@ -129,13 +128,11 @@ module Simplex.Chat.Store.Groups getRelayServedGroups, getRelayPublishableGroups, getRelayInactiveGroups, - createNewContactMemberAsync, createJoiningMember, getMemberJoinRequest, createJoiningMemberConnection, createBusinessRequestGroup, getContactViaMember, - setNewContactMemberConnRequest, getMemberInvitation, createMemberConnection, createMemberConnectionAsync, @@ -354,11 +351,6 @@ getGroupLink db User {userId} gInfo@GroupInfo {groupId} = acceptMemberRole = fromMaybe GRMember mRole_ } -getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId) -getGroupLinkId db User {userId} GroupInfo {groupId} = - fmap join . maybeFirstRow fromOnly $ - DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) - setGroupLinkMemberRole :: DB.Connection -> User -> GroupLink -> GroupMemberRole -> IO GroupLink setGroupLinkMemberRole db User {userId} gLnk@GroupLink{userContactLinkId} memberRole = do DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId) @@ -2038,33 +2030,6 @@ getRelayInactiveGroups db cxt User {userId, userContactId} ttl = do ) (userId, userContactId, RSInactive, cutoffTs) -createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> ExceptT StoreError IO () -createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo {groupId, membership} Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) chatV peerChatVRange subMode = - createWithRandomId' db gVar $ \memId -> runExceptT $ do - createdAt <- liftIO getCurrentTime - insertMember_ (MemberId memId) createdAt - groupMemberId <- liftIO $ insertedRowId db - Connection {connId} <- liftIO $ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 createdAt subMode - liftIO $ setCommandConnId db user cmdId connId - where - VersionRange minV maxV = peerChatVRange - insertMember_ memberId createdAt = do - indexInGroup <- getUpdateNextIndexInGroup_ db groupId - liftIO $ - DB.execute - db - [sql| - INSERT INTO group_members - ( group_id, index_in_group, member_id, member_role, member_category, member_status, member_relations_vector, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, - peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) - |] - ( (groupId, indexInGroup, memberId, memberRole, GCInviteeMember, GSMemInvited, Binary B.empty, fromInvitedBy userContactId IBUser, groupMemberId' membership) - :. (userId, localDisplayName, contactId, localProfileId profile, createdAt, createdAt) - :. (minV, maxV) - ) - createJoiningMember :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupMemberRole -> GroupMemberStatus -> Maybe MemberKey -> ExceptT StoreError IO (GroupMemberId, MemberId) createJoiningMember db @@ -2224,11 +2189,6 @@ getContactViaMember db cxt user@User {userId} GroupMember {groupMemberId} = do (userId, groupMemberId) getContact db cxt user contactId -setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO () -setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do - currentTs <- getCurrentTime - DB.execute db "UPDATE group_members SET sent_inv_queue_info = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?" (connRequest, currentTs, userId, groupMemberId) - getMemberInvitation :: DB.Connection -> User -> Int64 -> IO (Maybe ConnReqInvitation) getMemberInvitation db User {userId} groupMemberId = fmap join . maybeFirstRow fromOnly $ @@ -3287,7 +3247,7 @@ createMemberContact quotaErrCounter = 0 } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactRequest = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} getMemberContact :: DB.Connection -> StoreCxt -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact db cxt user contactId = do diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 7b71e61512..773d9c1f32 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1141,7 +1141,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = do [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 3131cbd245..19c07edbf8 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -45,6 +45,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code import Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description import Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history import Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles +import Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -89,7 +90,8 @@ schemaMigrations = ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code), ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description), ("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history), - ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles) + ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles), + ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260723_contact_request_rejection.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260723_contact_request_rejection.hs new file mode 100644 index 0000000000..1aa8d1ff27 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260723_contact_request_rejection.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260723_contact_request_rejection where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260723_contact_request_rejection :: Text +m20260723_contact_request_rejection = + [r| +ALTER TABLE contact_requests ADD COLUMN rejection_supported SMALLINT NOT NULL DEFAULT 0; +|] + +down_m20260723_contact_request_rejection :: Text +down_m20260723_contact_request_rejection = + [r| +ALTER TABLE contact_requests DROP COLUMN rejection_supported; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index e901c8858e..ae695d3c37 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -580,7 +580,8 @@ CREATE TABLE test_chat_schema.contact_requests ( contact_id bigint, business_group_id bigint, welcome_shared_msg_id bytea, - request_shared_msg_id bytea + request_shared_msg_id bytea, + rejection_supported smallint DEFAULT 0 NOT NULL ); @@ -993,7 +994,7 @@ CREATE TABLE test_chat_schema.groups ( public_member_count bigint, relay_request_retries bigint DEFAULT 0 NOT NULL, relay_request_delay bigint DEFAULT 0 NOT NULL, - relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 04:00:00+04'::timestamp with time zone NOT NULL, + relay_request_execute_at timestamp with time zone DEFAULT '1970-01-01 01:00:00+01'::timestamp with time zone NOT NULL, relay_inactive_at timestamp with time zone, relay_sent_web_domain text, roster_version bigint, diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 7fc18617e7..a4e7ab04a2 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -168,6 +168,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code import Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description import Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history import Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles +import Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -335,7 +336,8 @@ schemaMigrations = ("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code), ("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description), ("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history), - ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles) + ("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles), + ("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260723_contact_request_rejection.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260723_contact_request_rejection.hs new file mode 100644 index 0000000000..7aa36ea5ee --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260723_contact_request_rejection.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260723_contact_request_rejection where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260723_contact_request_rejection :: Query +m20260723_contact_request_rejection = + [sql| +ALTER TABLE contact_requests ADD COLUMN rejection_supported INTEGER NOT NULL DEFAULT 0; +|] + +down_m20260723_contact_request_rejection :: Query +down_m20260723_contact_request_rejection = + [sql| +ALTER TABLE contact_requests DROP COLUMN rejection_supported; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index a986773cb2..4438182941 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -286,7 +286,7 @@ SEARCH c USING COVERING INDEX idx_connections_user (user_id=?) Query: SELECT user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, - last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support + last_external_snd_msg_id, deleted, ratchet_sync_state, pq_support, service_request_expires_at FROM connections WHERE conn_id = ? AND deleted = ? @@ -310,7 +310,7 @@ Plan: Query: INSERT INTO conn_invitations - (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0); + (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted, service_request) VALUES (?, ?, ?, ?, 0, ?); Plan: @@ -429,8 +429,8 @@ Query: WHERE cs.deleted = 0 Plan: -SCAN c USING INDEX idx_commands_conn_id -SEARCH cs USING PRIMARY KEY (conn_id=?) +SEARCH cs USING COVERING INDEX idx_connections_deleted (deleted=?) +SEARCH c USING INDEX idx_commands_conn_id (conn_id=?) SEARCH s USING PRIMARY KEY (host=? AND port=?) LEFT-JOIN USE TEMP B-TREE FOR DISTINCT @@ -441,8 +441,8 @@ Query: JOIN connections c ON q.conn_id = c.conn_id WHERE c.deleted = 0 AND q.deleted = 0 Plan: -SCAN q USING INDEX idx_rcv_queues_client_notice_id -SEARCH c USING PRIMARY KEY (conn_id=?) +SEARCH c USING INDEX idx_connections_deleted (deleted=?) +SEARCH q USING INDEX idx_rcv_queue_id (conn_id=?) SEARCH s USING PRIMARY KEY (host=? AND port=?) USE TEMP B-TREE FOR DISTINCT @@ -504,7 +504,7 @@ Plan: SEARCH conn_confirmations USING PRIMARY KEY (confirmation_id=?) Query: - SELECT contact_conn_id, cr_invitation, recipient_conn_info, own_conn_info, accepted + SELECT contact_conn_id, cr_invitation, recipient_conn_info, own_conn_info, accepted, service_request, created_at FROM conn_invitations WHERE invitation_id = ? AND accepted = 0 @@ -538,6 +538,17 @@ Plan: SEARCH n USING INDEX idx_client_notices_entity (protocol=?) SEARCH s USING PRIMARY KEY (host=? AND port=?) +Query: + SELECT ratchet_key_id, x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem + FROM address_ratchet_keys + WHERE conn_id = ? + ORDER BY address_ratchet_key_id DESC + LIMIT 1 + +Plan: +SEARCH address_ratchet_keys USING INDEX idx_address_ratchet_keys (conn_id=?) +USE TEMP B-TREE FOR ORDER BY + Query: SELECT s.internal_id, m.msg_type, s.internal_hash, s.rcpt_internal_id, s.rcpt_status FROM snd_messages s @@ -548,6 +559,31 @@ Plan: SEARCH s USING PRIMARY KEY (conn_id=? AND internal_snd_id=?) SEARCH m USING PRIMARY KEY (conn_id=? AND internal_id=?) +Query: + SELECT x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem + FROM address_ratchet_keys + WHERE conn_id = ? AND ratchet_key_id = ? + +Plan: +SEARCH address_ratchet_keys USING INDEX idx_address_ratchet_keys (conn_id=? AND ratchet_key_id=?) + +Query: + DELETE FROM address_ratchet_keys + WHERE conn_id = ? + AND address_ratchet_key_id NOT IN ( + SELECT address_ratchet_key_id + FROM address_ratchet_keys + WHERE conn_id = ? + ORDER BY address_ratchet_key_id DESC + LIMIT ? + ) + +Plan: +SEARCH address_ratchet_keys USING COVERING INDEX idx_address_ratchet_keys (conn_id=?) +LIST SUBQUERY 1 +SEARCH address_ratchet_keys USING COVERING INDEX idx_address_ratchet_keys (conn_id=?) +USE TEMP B-TREE FOR ORDER BY + Query: DELETE FROM conn_confirmations WHERE conn_id = ? @@ -555,9 +591,17 @@ Query: Plan: SEARCH conn_confirmations USING COVERING INDEX idx_conn_confirmations_conn_id (conn_id=?) +Query: + INSERT INTO address_ratchet_keys + (conn_id, ratchet_key_id, x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem) + VALUES (?, ?, ?, ?, ?) + +Plan: + Query: INSERT INTO connections - (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, pq_support, duplex_handshake) VALUES (?,?,?,?,?,?,?) + (user_id, conn_id, conn_mode, smp_agent_version, enable_ntfs, pq_support, service_request_expires_at, duplex_handshake, created_at) + VALUES (?,?,?,?,?,?,?,?,?) Plan: @@ -849,10 +893,10 @@ Query: Plan: MATERIALIZE d SCAN snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_expired -SCAN d -SEARCH q USING INDEX idx_snd_queue_id (conn_id=? AND snd_queue_id=?) +SEARCH c USING INDEX idx_connections_deleted (deleted=?) +SEARCH q USING INDEX idx_snd_queue_id (conn_id=?) +SEARCH d USING AUTOMATIC COVERING INDEX (conn_id=? AND snd_queue_id=?) SEARCH s USING PRIMARY KEY (host=? AND port=?) -SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT @@ -994,6 +1038,7 @@ SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?) Query: DELETE FROM connections WHERE conn_id = ? Plan: SEARCH connections USING PRIMARY KEY (conn_id=?) +SEARCH address_ratchet_keys USING COVERING INDEX idx_address_ratchet_keys (conn_id=?) SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_hash (conn_id=?) SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=?) SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=?) @@ -1008,6 +1053,7 @@ SEARCH rcv_queues USING COVERING INDEX idx_rcv_queue_id (conn_id=?) Query: DELETE FROM connections WHERE user_id = 2 Plan: SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?) +SEARCH address_ratchet_keys USING COVERING INDEX idx_address_ratchet_keys (conn_id=?) SEARCH processed_ratchet_key_hashes USING COVERING INDEX idx_processed_ratchet_key_hashes_hash (conn_id=?) SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=?) SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_conn_id_internal_id (conn_id=?) @@ -1143,6 +1189,10 @@ Plan: Query: INSERT INTO users DEFAULT VALUES Plan: +Query: SELECT 1 FROM connections WHERE conn_id = ? AND deleted_at_wait_delivery < ? LIMIT 1 +Plan: +SEARCH connections USING PRIMARY KEY (conn_id=?) + Query: SELECT 1 FROM encrypted_rcv_message_hashes WHERE conn_id = ? AND hash = ? LIMIT 1 Plan: SEARCH encrypted_rcv_message_hashes USING COVERING INDEX idx_encrypted_rcv_message_hashes_hash (conn_id=? AND hash=?) @@ -1157,7 +1207,7 @@ SEARCH snd_message_deliveries USING COVERING INDEX idx_snd_message_deliveries_ex Query: SELECT conn_id FROM connections WHERE deleted = 0 Plan: -SCAN connections +SEARCH connections USING COVERING INDEX idx_connections_deleted (deleted=?) Query: SELECT conn_id FROM connections WHERE user_id = ? Plan: @@ -1165,7 +1215,7 @@ SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?) Query: SELECT count(1) FROM connections Plan: -SCAN connections USING COVERING INDEX idx_connections_user +SCAN connections USING COVERING INDEX idx_connections_deleted Query: SELECT count(1) FROM snd_message_bodies Plan: diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 14e2299514..6884bf7e04 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -124,7 +124,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, cp.contact_domain, cp.contact_domain_proof, cp.contact_domain_verified, @@ -135,12 +135,14 @@ Query: FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = ct.contact_request_id WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 Plan: SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN +SEARCH cr2 USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN Query: SELECT @@ -269,8 +271,8 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: INSERT INTO contact_requests (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, - created_at, updated_at, xcontact_id, welcome_shared_msg_id, request_shared_msg_id, pq_support) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, xcontact_id, welcome_shared_msg_id, request_shared_msg_id, pq_support, rejection_supported) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -401,7 +403,7 @@ Plan: Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -731,18 +733,20 @@ Plan: Query: SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.welcome_shared_msg_id, c.request_shared_msg_id, c.contact_request_id, cr2.rejection_supported, c.contact_group_member_id, c.contact_grp_inv_sent, c.grp_direct_inv_link, c.grp_direct_inv_from_group_id, c.grp_direct_inv_from_group_member_id, c.grp_direct_inv_from_member_conn_id, c.grp_direct_inv_started_connection, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = c.contact_request_id WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0 Plan: SEARCH c USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cr2 USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN Query: SELECT @@ -1455,7 +1459,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, @@ -1467,6 +1471,7 @@ Query: FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN connections c ON c.contact_id = ct.contact_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = ct.contact_request_id WHERE ( (c.user_id = ? AND c.via_contact_uri_hash = ?) OR (c.user_id = ? AND c.via_contact_uri_hash = ?) @@ -1480,6 +1485,7 @@ INDEX 2 SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cr2 USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN Query: SELECT 1 FROM users @@ -2022,7 +2028,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, cp.display_name, cp.full_name, cp.short_descr, cp.description, cp.image, cp.contact_link, cp.chat_peer_type, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, cr2.rejection_supported, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.grp_direct_inv_link, ct.grp_direct_inv_from_group_id, ct.grp_direct_inv_from_group_member_id, ct.grp_direct_inv_from_member_conn_id, ct.grp_direct_inv_started_connection, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, cp.badge_proof, cp.badge_pres_header, cp.badge_expiry, cp.badge_type, cp.badge_verified, cp.badge_extra, cp.badge_master_key, cp.badge_signature, cp.badge_key_idx, @@ -2034,6 +2040,7 @@ Query: FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id + LEFT JOIN contact_requests cr2 ON cr2.contact_request_id = ct.contact_request_id WHERE ct.user_id = ? AND ct.contact_id = ? AND ct.deleted = ? @@ -2041,6 +2048,7 @@ Plan: SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN +SEARCH cr2 USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN Query: SELECT @@ -2107,7 +2115,7 @@ USE TEMP B-TREE FOR ORDER BY Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -2137,7 +2145,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -2167,7 +2175,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -4767,13 +4775,6 @@ Query: Plan: -Query: - INSERT INTO chat_item_moderations - (group_id, moderator_member_id, item_member_id, shared_msg_id, created_by_msg_id, moderated_at) - VALUES (?,?,?,?,?,?) - -Plan: - Query: INSERT INTO chat_item_versions (chat_item_id, msg_content, item_version_ts) VALUES (?,?,?) @@ -5660,7 +5661,7 @@ SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -5677,7 +5678,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, - cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, cr.rejection_supported, cr.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, @@ -6477,10 +6478,6 @@ Query: DELETE FROM chat_item_mentions WHERE chat_item_id = ? Plan: SEARCH chat_item_mentions USING COVERING INDEX idx_chat_item_mentions_chat_item_id (chat_item_id=?) -Query: DELETE FROM chat_item_moderations WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ? -Plan: -SEARCH chat_item_moderations USING COVERING INDEX idx_chat_item_moderations_group (group_id=? AND item_member_id=? AND shared_msg_id=?) - Query: DELETE FROM chat_item_reactions WHERE contact_id = ? Plan: SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_contact_id (contact_id=?) @@ -7170,6 +7167,10 @@ Plan: SCAN m USING COVERING INDEX idx_group_members_user_id_local_display_name SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) +Query: SELECT chat_item_id FROM chat_items WHERE group_scope_tag = 'member_support' ORDER BY chat_item_id DESC LIMIT 1 +Plan: +SCAN chat_items + Query: SELECT chat_item_id FROM chat_items WHERE item_text LIKE '%' || ? || '%' ORDER BY chat_item_id DESC LIMIT 1 Plan: SCAN chat_items diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index e42b537225..cfbcf70599 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -428,6 +428,7 @@ CREATE TABLE contact_requests( business_group_id INTEGER REFERENCES groups(group_id) ON DELETE CASCADE, welcome_shared_msg_id BLOB, request_shared_msg_id BLOB, + rejection_supported INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON UPDATE CASCADE diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 050cf7dc97..6516b3cc66 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -488,22 +488,23 @@ type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Ma type GroupDirectInvitationRow = (Maybe ConnReqInvitation, Maybe GroupId, Maybe GroupMemberId, Maybe Int64, BoolInt) -type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow +type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe BoolInt, Maybe GroupMemberId, BoolInt) :. GroupDirectInvitationRow :. (Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) :. BadgeRow :. ContactDomainRow type ContactRow = Only ContactId :. ContactRow' type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt) toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = +toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, localBadge = rowToBadge now badgeRow, preferences, localAlias} activeConn = toMaybeConnection cxt connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito preparedContact = toPreparedContact preparedContactRow + contactRequest = UserContactRequestRef <$> contactRequestId <*> (unBI <$> rejectionSupported_) groupDirectInv = toGroupDirectInvitation groupDirectInvRow - in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactRequest, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} rowToContactDomain :: ContactDomainRow -> Maybe SimplexDomainClaim rowToContactDomain (domain_, domainProof_, _) = (`SimplexDomainClaim` domainProof_) . StrJSON <$> domain_ @@ -544,13 +545,13 @@ getProfileById db userId profileId = do |] (userId, profileId) -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64, BoolInt) :. (Int64, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) :. BadgeRow :. ContactDomainRow toContactRequest :: UTCTime -> ContactRequestRow -> UserContactRequest -toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do +toContactRequest now ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, BI rejectionSupported) :. (profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer) :. badgeRow :. domainRow) = do let profile = LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, contactDomain = rowToContactDomain domainRow, contactDomainVerified = rowToDomainVerified domainRow, peerType, preferences, localBadge = rowToBadge now badgeRow, localAlias} cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} + in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt, rejectionSupported} userQuery :: Query userQuery = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index dce1a2a9c9..7bccd04b71 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -60,7 +60,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AConnectionLink (..), AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink (..), ConnectionMode (..), ConnectionModeI, ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), SConnectionMode (..), SimplexDomain, SimplexNameInfo (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AConnectionLink (..), AEventTag (..), AEvtTag (..), ConnId, ConnShortLink (..), ConnectionLink (..), ConnectionMode (..), ConnectionModeI, ConnectionRequestUri, ContactConnType (..), CreatedConnLink (..), InvitationId, SAEntity (..), SConnectionMode (..), SimplexDomain, SimplexNameInfo (..), UserId, sConnectionMode) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) @@ -205,6 +205,7 @@ data Contact = Contact chatTs :: Maybe UTCTime, preparedContact :: Maybe PreparedContact, contactRequestId :: Maybe Int64, + contactRequest :: Maybe UserContactRequestRef, -- contactGroupMemberId + contactGrpInvSent are used in conjunction for making connection request -- to a group member via direct message feature contactGroupMemberId :: Maybe GroupMemberId, @@ -232,6 +233,12 @@ data PreparedContact = PreparedContact } deriving (Eq, Show) +data UserContactRequestRef = UserContactRequestRef + { contactRequestId :: Int64, + rejectionSupported :: Bool + } + deriving (Eq, Show) + data GroupDirectInvitation = GroupDirectInvitation { groupDirectInvLink :: ConnReqInvitation, fromGroupId_ :: Maybe GroupId, @@ -316,6 +323,7 @@ data ContactStatus = CSActive | CSDeleted | CSDeletedByUser + | CSRejected deriving (Eq, Show, Ord) instance FromField ContactStatus where fromField = fromTextField_ textDecode @@ -334,11 +342,13 @@ instance TextEncoding ContactStatus where "active" -> Just CSActive "deleted" -> Just CSDeleted "deletedByUser" -> Just CSDeletedByUser + "rejected" -> Just CSRejected _ -> Nothing textEncode = \case CSActive -> "active" CSDeleted -> "deleted" CSDeletedByUser -> "deletedByUser" + CSRejected -> "rejected" data ContactRef = ContactRef { contactId :: ContactId, @@ -383,7 +393,8 @@ data UserContactRequest = UserContactRequest xContactId :: Maybe XContactId, pqSupport :: PQSupport, welcomeSharedMsgId :: Maybe SharedMsgId, - requestSharedMsgId :: Maybe SharedMsgId + requestSharedMsgId :: Maybe SharedMsgId, + rejectionSupported :: Bool } deriving (Eq, Show) @@ -708,7 +719,7 @@ data Profile = Profile } deriving (Eq, Show) -data ChatPeerType = CPTHuman | CPTBot +data ChatPeerType = CPTHuman | CPTBot | CPTBusiness | CPTUnknown Text deriving (Eq, Show) instance FromJSON ChatPeerType where @@ -723,13 +734,16 @@ instance FromField ChatPeerType where fromField = fromTextField_ textDecode instance ToField ChatPeerType where toField = toField . textEncode instance TextEncoding ChatPeerType where - textDecode = \case - "human" -> Just CPTHuman - "bot" -> Just CPTBot - _ -> Nothing + textDecode s = Just $ case s of + "human" -> CPTHuman + "bot" -> CPTBot + "business" -> CPTBusiness + tag -> CPTUnknown tag textEncode = \case CPTHuman -> "human" CPTBot -> "bot" + CPTBusiness -> "business" + CPTUnknown tag -> tag profileFromName :: ContactName -> Profile profileFromName displayName = @@ -992,6 +1006,26 @@ instance ToJSON GroupRejectionReason where toJSON = strToJSON toEncoding = strToJEncoding +data ContactRejectionReason + = CRRUserRejected + | CRRUnknown {text :: Text} + deriving (Eq, Show) + +instance StrEncoding ContactRejectionReason where + strEncode = \case + CRRUserRejected -> "user_rejected" + CRRUnknown text -> encodeUtf8 text + strP = + "user_rejected" $> CRRUserRejected + <|> CRRUnknown . safeDecodeUtf8 <$> A.takeByteString + +instance FromJSON ContactRejectionReason where + parseJSON = strParseJSON "ContactRejectionReason" + +instance ToJSON ContactRejectionReason where + toJSON = strToJSON + toEncoding = strToJEncoding + data RelayRejectionReason = RRRRejoinRejected | RRRUnknown {text :: Text} @@ -1830,6 +1864,17 @@ instance StrEncoding AConnectTarget where where nameStart = "@" <|> "#" <|> "simplex:/name" +instance ConnectionModeI m => StrEncoding (ConnectTarget m) where + strEncode t = strEncode $ ACTarget sConnectionMode t + strP = connectTargetP + +connectTargetP :: forall m. ConnectionModeI m => A.Parser (ConnectTarget m) +connectTargetP = do + ACTarget m t <- strP + case testEquality m (sConnectionMode :: SConnectionMode m) of + Just Refl -> pure t + Nothing -> fail "bad connect target mode" + aConnectTarget :: AConnectionLink -> AConnectTarget aConnectTarget (ACL SCMInvitation cl) = ACTarget SCMInvitation (CTInv cl) aConnectTarget (ACL SCMContact cl) = ACTarget SCMContact $ case cl of @@ -2071,7 +2116,7 @@ instance TextEncoding CommandStatus where data CommandFunction = CFCreateConnGrpMemInv - | CFCreateConnGrpInv + | CFCreateConnGrpInv -- deprecated | CFCreateConnFileInvDirect -- deprecated | CFCreateConnFileInvGroup -- deprecated | CFJoinConn @@ -2204,7 +2249,7 @@ peerConnChatVersion _local@(VersionRange lmin lmax) _peer@(VersionRange rmin rma | otherwise = rmax initialChatVersion :: VersionChat -initialChatVersion = VersionChat 1 +initialChatVersion = VersionChat 9 chatInitialVRange :: VersionRangeChat chatInitialVRange = versionToRange initialChatVersion @@ -2342,6 +2387,8 @@ $(JQ.deriveJSON defaultJSON ''FileTransferMeta) $(JQ.deriveJSON defaultJSON ''PreparedContact) +$(JQ.deriveJSON defaultJSON ''UserContactRequestRef) + $(JQ.deriveJSON defaultJSON ''GroupDirectInvitation) $(JQ.deriveJSON defaultJSON ''LocalFileMeta) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 3cf7554cf5..5894d12fd3 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -112,7 +112,7 @@ 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 hu cfg@ChatConfig {logLevel, showReactions, testView} liveItems ts tz outputRH = \case +chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, testView} liveItems ts tz outputRH = \case CRActiveUser User {profile = p@LocalProfile {localBadge}, uiThemes} -> viewUserProfile localBadge (fromLocalProfile p) <> viewUITheme uiThemes CRUsersList users -> viewUsersList users CRChatStarted -> ["chat started"] @@ -179,9 +179,11 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte HSDatabase -> databaseHelpInfo CRWelcome user -> chatWelcome user CRContactsList u cs -> ttyUser u $ viewContactsList cs - CRUserContactLink u UserContactLink {connLinkContact, addressSettings} -> ttyUser u $ connReqContact_ "Your chat address:" connLinkContact <> viewAddressSettings addressSettings + CRUserContactLink u UserContactLink {connLinkContact, addressSettings} -> ttyUser u $ connReqContact_ showFullLinks "Your chat address:" connLinkContact <> viewAddressSettings addressSettings CRUserContactLinkUpdated u UserContactLink {addressSettings} -> ttyUser u $ viewAddressSettings addressSettings CRContactRequestRejected u UserContactRequest {localDisplayName = c} _ct_ -> ttyUser u [ttyContact c <> ": contact request rejected"] + CRServiceResponse u resp -> ttyUser u ["service response: " <> viewJSON resp] + CRServiceReplyAccepted u (AgentConnId cId) -> ttyUser u [plain $ "service reply accepted, connection id: " <> safeDecodeUtf8 (strEncode cId)] CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView CRPublicGroupCreationFailed u results -> ttyUser u $ viewPublicGroupCreationFailed results @@ -201,9 +203,9 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRUserProfileNoChange u -> ttyUser u ["user profile did not change"] CRUserPrivacy u u' -> ttyUserPrefix hu outputRH u $ viewUserPrivacy u u' CRVersionInfo info _ _ -> viewVersionInfo logLevel info - CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation ccLink + CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation showFullLinks ccLink CRConnectionIncognitoUpdated u c customUserProfile -> ttyUser u $ viewConnectionIncognitoUpdated c customUserProfile testView - CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' + CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged showFullLinks u c nu c' CRConnectionPlan u connLink _ otherSimplexName connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan <> otherSimplexNameNote otherSimplexName CRNewPreparedChat u (AChat _ (Chat cInfo _ _)) -> ttyUser u $ case cInfo of DirectChat ct -> [ttyContact' ct <> ": contact is prepared"] @@ -221,7 +223,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] - CRUserContactLinkCreated u ccLink -> ttyUser u $ connReqContact_ "Your new chat address is created!" ccLink + CRUserContactLinkCreated u ccLink -> ttyUser u $ connReqContact_ showFullLinks "Your new chat address is created!" ccLink CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] CRUserDeletedMembers u g members wm signed -> case members of @@ -260,8 +262,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRGroupUpdated u g g' m signed -> ttyUser u $ viewGroupUpdated g g' m (if signed then Just MSSVerified else Nothing) CRGroupProfile u g -> ttyUser u $ viewGroupProfile g CRGroupDescription u g -> ttyUser u $ viewGroupDescription g - CRGroupLinkCreated u g gLink -> ttyUser u $ groupLink_ "Group link is created!" g gLink - CRGroupLink u g gLink -> ttyUser u $ groupLink_ "Group link:" g gLink + CRGroupLinkCreated u g gLink -> ttyUser u $ groupLink_ showFullLinks "Group link is created!" g gLink + CRGroupLink u g gLink -> ttyUser u $ groupLink_ showFullLinks "Group link:" g gLink CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m] @@ -445,7 +447,6 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtGroupChatItemsDeleted u g ciIds byUser member_ -> ttyUser u $ viewGroupChatItemsDeleted g ciIds byUser member_ CEvtChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] CEvtUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] - CEvtSentGroupInvitation u g c _ -> ttyUser u $ viewSentGroupInvitation g c CEvtContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CEvtAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c CEvtAcceptingBusinessRequest u g -> ttyUser u $ viewAcceptingBusinessRequest g @@ -463,6 +464,13 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c' CEvtGroupMemberUpdated {} -> [] CEvtReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} _chat -> ttyUser u $ viewReceivedContactRequest c (fromLocalProfile profile) + CEvtServiceRequest u reqId sigKey_ req -> + ttyUser u $ + [plain $ "service request " <> safeDecodeUtf8 (strEncode reqId)] + <> maybe [] (\k -> [plain $ "signed by " <> safeDecodeUtf8 (strEncode k)]) sigKey_ + <> ["request: " <> viewJSON req] + CEvtServiceReplySent (AgentConnId cId) -> [plain $ "service reply sent, connection id: " <> safeDecodeUtf8 (strEncode cId)] + CEvtContactRequestRejected u Contact {localDisplayName = c} _reason -> ttyUser u [ttyContact c <> ": contact request rejected"] CEvtRcvFileStart u ci -> ttyUser u $ receivingFile_' hu testView "started" ci CEvtRcvFileComplete u ci -> ttyUser u $ receivingFile_' hu testView "completed" ci CEvtRcvStandaloneFileComplete u _ ft -> ttyUser u $ receivingFileStandalone "completed" ft @@ -1039,8 +1047,8 @@ viewInvalidConnReq = plain updateStr ] -viewConnReqInvitation :: CreatedLinkInvitation -> [StyledString] -viewConnReqInvitation (CCLink cReq shortLink) = +viewConnReqInvitation :: Bool -> CreatedLinkInvitation -> [StyledString] +viewConnReqInvitation showFullLinks (CCLink cReq shortLink) = [ "pass this invitation link to your contact (via another channel): ", "", plain $ maybe cReqStr strEncode shortLink, @@ -1048,7 +1056,7 @@ viewConnReqInvitation (CCLink cReq shortLink) = "and ask them to connect: " <> highlight' "/c " ] <> - if isJust shortLink + if showFullLinks && isJust shortLink then [ "The invitation link for old clients:", plain cReqStr @@ -1111,8 +1119,8 @@ viewForwardPlan count itemIds = maybe [forwardCount] $ \fc -> [confirmation fc, | otherwise = plain $ show len <> " message(s) out of " <> show count <> " can be forwarded" len = length itemIds -connReqContact_ :: StyledString -> CreatedLinkContact -> [StyledString] -connReqContact_ intro (CCLink cReq shortLink) = +connReqContact_ :: Bool -> StyledString -> CreatedLinkContact -> [StyledString] +connReqContact_ showFullLinks intro (CCLink cReq shortLink) = [ intro, "", plain $ maybe cReqStr strEncode shortLink, @@ -1122,16 +1130,16 @@ connReqContact_ intro (CCLink cReq shortLink) = "to share with your contacts: " <> highlight' "/profile_address on", "to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)" ] - <> ["The contact link for old clients: " <> plain cReqStr | isJust shortLink] + <> ["The contact link for old clients: " <> plain cReqStr | showFullLinks, isJust shortLink] where cReqStr = strEncode $ simplexChatContact cReq simplexChatContact :: ConnReqContact -> ConnReqContact -simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simplexChat} +simplexChatContact (CRContactUri crData e2e) = CRContactUri crData {crScheme = simplexChat} e2e simplexChatContact' :: ConnLinkContact -> ConnLinkContact simplexChatContact' = \case - CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat} + CLFull (CRContactUri crData e2e) -> CLFull $ CRContactUri crData {crScheme = simplexChat} e2e l@(CLShort _) -> l groupSimplexDomain :: GroupInfo -> Maybe SimplexDomain @@ -1172,8 +1180,8 @@ viewAddressSettings AddressSettings {businessAddress, autoAccept, autoReply} = c | otherwise = "" _ -> ["auto_accept off"] -groupLink_ :: StyledString -> GroupInfo -> GroupLink -> [StyledString] -groupLink_ intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} = +groupLink_ :: Bool -> StyledString -> GroupInfo -> GroupLink -> [StyledString] +groupLink_ showFullLinks intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} = [ intro, "", plain $ maybe cReqStr strEncode shortLink @@ -1184,7 +1192,7 @@ groupLink_ intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMem "to show it again: " <> highlight ("/show link #" <> viewGroupName g), "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" ] - <> ["The group link for old clients: " <> plain cReqStr | isJust shortLink] + <> ["The group link for old clients: " <> plain cReqStr | showFullLinks, isJust shortLink] where cReqStr = strEncode $ simplexChatContact cReq @@ -2116,8 +2124,8 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr Nothing -> ["unexpected response when changing connection, please report to developers"] | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] -viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] -viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId} User {localDisplayName = n'} PendingContactConnection {connLinkInv = connLinkInv'} = +viewConnectionUserChanged :: Bool -> User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] +viewConnectionUserChanged showFullLinks User {localDisplayName = n} PendingContactConnection {pccConnId} User {localDisplayName = n'} PendingContactConnection {connLinkInv = connLinkInv'} = case connLinkInv' of Just ccLink' -> [userChangedStr <> ", new link:"] <> newLink ccLink' _ -> [userChangedStr] @@ -2129,7 +2137,7 @@ viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection { "" ] <> - if isJust shortLink + if showFullLinks && isJust shortLink then [ "The invitation link for old clients:", plain cReqStr diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 77d91bf522..76e808d816 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -37,7 +37,6 @@ import Simplex.Chat.Library.Commands import Simplex.Chat.Operators import Simplex.Chat.Options import Simplex.Chat.Options.DB -import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) import Simplex.Chat.Store import Simplex.Chat.Store.Profiles import Simplex.Chat.Terminal @@ -51,7 +50,7 @@ import Simplex.FileTransfer.Server.Store import Simplex.FileTransfer.Transport (alpnSupportedXFTPhandshakes, supportedFileServerVRange) import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite -import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) +import Simplex.Messaging.Agent.Protocol (duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.Entity (SDBStored (..)) import Simplex.Messaging.Agent.Store.Interface (closeDBStore) @@ -122,6 +121,7 @@ testOpts = optFilesFolder = Nothing, optTempDirectory = Nothing, showReactions = True, + showFullLinks = True, allowInstantFiles = True, autoAcceptFileSize = 0, muteNotifications = True, @@ -173,6 +173,12 @@ testCoreOpts = relayTestOpts :: ChatOpts relayTestOpts = testOpts {coreOptions = testCoreOpts {chatRelay = True}} +testOptsNoFullLinks :: ChatOpts +testOptsNoFullLinks = testOpts {showFullLinks = False} + +relayTestOptsNoFullLinks :: ChatOpts +relayTestOptsNoFullLinks = relayTestOpts {showFullLinks = False} + relayWebTestOpts :: Text -> FilePath -> Maybe FilePath -> ChatOpts relayWebTestOpts webDomain webDir webCorsFile = testOpts {coreOptions = testCoreOpts {chatRelay = True, webPreviewConfig = Just WebPreviewConfig {webDomain, webJsonDir = webDir, webCorsFile, webUpdateInterval = 300, webPreviewItemCount = 50}}} @@ -185,7 +191,7 @@ termSettings :: VirtualTerminalSettings termSettings = VirtualTerminalSettings { virtualType = "xterm", - virtualWindowSize = pure C.Size {height = 24, width = 6000}, + virtualWindowSize = pure C.Size {height = 24, width = 7500}, virtualEvent = retry, virtualInterrupt = retry } @@ -232,21 +238,12 @@ testAgentCfgVPrev = smpCfg = (smpCfg testAgentCfg) {serverVRange = prevRange $ serverVRange $ smpCfg testAgentCfg} } -testAgentCfgVNext :: AgentConfig -testAgentCfgVNext = - testAgentCfg - { smpClientVRange = nextRange $ smpClientVRange testAgentCfg, - smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion $ max pqdrSMPAgentVersion currentSMPAgentVersion, - e2eEncryptVRange = mkVersionRange CR.kdfX3DHE2EEncryptVersion $ max CR.pqRatchetE2EEncryptVersion CR.currentE2EEncryptVersion, - smpCfg = (smpCfg testAgentCfg) {serverVRange = nextRange $ serverVRange $ smpCfg testAgentCfg} - } - testAgentCfgV1 :: AgentConfig testAgentCfgV1 = testAgentCfg { smpClientVRange = v1Range, - smpAgentVRange = versionToRange duplexHandshakeSMPAgentVersion, - e2eEncryptVRange = versionToRange CR.kdfX3DHE2EEncryptVersion, + smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion pqdrSMPAgentVersion, + e2eEncryptVRange = mkVersionRange CR.kdfX3DHE2EEncryptVersion CR.pqRatchetE2EEncryptVersion, smpCfg = (smpCfg testAgentCfg) {serverVRange = versionToRange minClientSMPRelayVersion} } @@ -257,17 +254,10 @@ testCfgVPrev = agentConfig = testAgentCfgVPrev } -testCfgVNext :: ChatConfig -testCfgVNext = - testCfg - { chatVRange = mkVersionRange initialChatVersion $ max pqEncryptionCompressionVersion currentChatVersion, - agentConfig = testAgentCfgVNext - } - testCfgV1 :: ChatConfig testCfgV1 = testCfg - { chatVRange = v1Range, + { chatVRange = chatInitialVRange, agentConfig = testAgentCfgV1 } diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index e34f071fd0..a828d428d3 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -21,7 +21,7 @@ import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB -import Data.List (intercalate) +import Data.List (intercalate, stripPrefix) import qualified Data.Text as T import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS @@ -35,7 +35,9 @@ import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (NetworkTimeout (..)) +import Control.Concurrent.STM (atomically) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding.String (strEncode) import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport import Simplex.Messaging.Util (safeDecodeUtf8) @@ -119,6 +121,10 @@ chatDirectTests = do it "create second user" testCreateSecondUser it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart it "both users have contact link" testMultipleUserAddresses + it "service request and response over a DR address" testServiceRequestResponse + it "signed service request delivers the verified key" testSignedServiceRequest + it "service request dropped when service processing is off" testServiceRequestDroppedWhenOff + it "service request to a non-DR address fails fast" testServiceRequestNonDRAddress it "create user with same servers" testCreateUserSameServers it "delete user" testDeleteUser it "delete user with chat tags" testDeleteUserChatTags @@ -1894,6 +1900,77 @@ testUsersSubscribeAfterRestart ps = do bob #> "@alice hey alice" (alice, "alice") $<# "bob> hey alice" +testServiceRequestResponse :: HasCallStack => TestParams -> IO () +testServiceRequestResponse = + testChat2 aliceProfile bobProfile $ \alice bob -> do + alice ##> "/ad pq_ratchet=on" + (sLink, _) <- getContactLinks alice True + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/_start main=on snd_files=on service_requests=on" + alice <## "chat started" + concurrently_ + ( do + bob ##> ("/_service_request 1 " <> sLink <> " {\"ping\":1}") + bob <## "service response: {\"pong\":2}" + ) + ( do + reqId <- serviceRequestId alice + alice <## "request: {\"ping\":1}" + alice ##> ("/_service_response 1 " <> reqId <> " {\"pong\":2}") + replyConnId <- serviceReplyConnId alice + alice <## ("service reply sent, connection id: " <> replyConnId) + ) + where + serviceRequestId cc = getTermLine cc >>= maybe (serviceRequestId cc) pure . stripPrefix "service request " + serviceReplyConnId cc = getTermLine cc >>= maybe (serviceReplyConnId cc) pure . stripPrefix "service reply accepted, connection id: " + +testSignedServiceRequest :: HasCallStack => TestParams -> IO () +testSignedServiceRequest = + testChat2 aliceProfile bobProfile $ \alice bob -> do + alice ##> "/ad pq_ratchet=on" + (sLink, _) <- getContactLinks alice True + alice ##> "/_stop" + alice <## "chat stopped" + alice ##> "/_start main=on snd_files=on service_requests=on" + alice <## "chat started" + g <- C.newRandom + (pub, priv :: C.PrivateKeyEd25519) <- atomically $ C.generateKeyPair g + let signKey = B.unpack $ strEncode $ C.StoredPrivateKey priv + pubStr = B.unpack $ strEncode pub + concurrently_ + ( do + bob ##> ("/_service_request 1 " <> sLink <> " sign_key=" <> signKey <> " {\"ping\":1}") + bob <## "service response: {\"pong\":2}" + ) + ( do + reqId <- serviceRequestId alice + alice <## ("signed by " <> pubStr) + alice <## "request: {\"ping\":1}" + alice ##> ("/_service_response 1 " <> reqId <> " {\"pong\":2}") + replyConnId <- serviceReplyConnId alice + alice <## ("service reply sent, connection id: " <> replyConnId) + ) + where + serviceRequestId cc = getTermLine cc >>= maybe (serviceRequestId cc) pure . stripPrefix "service request " + serviceReplyConnId cc = getTermLine cc >>= maybe (serviceReplyConnId cc) pure . stripPrefix "service reply accepted, connection id: " + +testServiceRequestDroppedWhenOff :: HasCallStack => TestParams -> IO () +testServiceRequestDroppedWhenOff = + testChat2 aliceProfile bobProfile $ \alice bob -> do + alice ##> "/ad pq_ratchet=on" + (sLink, _) <- getContactLinks alice True + bob ##> ("/_service_request 1 " <> sLink <> " timeout=2 {\"ping\":1}") + bob <## "smp agent error: AGENT {agentErr = A_SERVICE {serviceError = ASETimeout}}" + +testServiceRequestNonDRAddress :: HasCallStack => TestParams -> IO () +testServiceRequestNonDRAddress = + testChat2 aliceProfile bobProfile $ \alice bob -> do + alice ##> "/ad" + (sLink, _) <- getContactLinks alice True + bob ##> ("/_service_request 1 " <> sLink <> " {\"ping\":1}") + bob <## "smp agent error: AGENT {agentErr = A_SERVICE {serviceError = ASENotDRAddress}}" + testMultipleUserAddresses :: HasCallStack => TestParams -> IO () testMultipleUserAddresses = testChat3 aliceProfile bobProfile cathProfile $ @@ -3323,17 +3400,17 @@ testUpdatePeerChatVRange ps = cfg11 = testCfg {chatVRange = vr11} :: ChatConfig vr11 :: VersionRangeChat -vr11 = mkVersionRange (VersionChat 1) (VersionChat 1) +vr11 = mkVersionRange (VersionChat 9) (VersionChat 9) contactInfoChatVRange :: TestCC -> VersionRangeChat -> IO () -contactInfoChatVRange cc (VersionRange minVer maxVer) = do +contactInfoChatVRange cc vr = do cc <## "contact ID: 2" cc <## "receiving messages via: localhost" cc <## "sending messages via: localhost" cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" cc <## "quantum resistant end-to-end encryption" - cc <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")") + cc <## ("peer chat protocol version range: " <> vRangeStr vr) testLinkContentFilter :: HasCallStack => TestParams -> IO () testLinkContentFilter = diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 37827ab2ad..2c01f379ad 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -97,8 +97,8 @@ chatGroupTests = do it "moderate own message (should process as deletion)" testGroupModerateOwn it "moderate multiple messages" testGroupModerateMultiple it "moderate message of another group member (full delete)" testGroupModerateFullDelete - it "moderate message that arrives after the event of moderation" testGroupDelayedModeration - it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete + xit "moderate message that arrives after the event of moderation" testGroupDelayedModeration + xit "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete it "remove member with messages (full deletion is enabled)" testDeleteMemberWithMessages it "remove member with messages mark deleted" testDeleteMemberMarkMessagesDeleted it "remove member - delete messages of left/removed members" testDeleteMemberMessagesLeftRemoved @@ -1805,7 +1805,7 @@ testGroupModerateOwn = \alice bob -> do createGroup2 "team" alice bob -- disableFullDeletion2 "team" alice bob - threadDelay 1000000 + threadDelay 1250000 alice #> "#team hello" bob <# "#team alice> hello" alice ##> "\\\\ #team @alice hello" @@ -2710,7 +2710,7 @@ testPlanGroupLinkLeaveRejoin = testGroupLink :: HasCallStack => TestParams -> IO () testGroupLink = - testChat3 aliceProfile bobProfile cathProfile $ + testChatOpts3 testOptsNoFullLinks aliceProfile bobProfile cathProfile $ \alice bob cath -> do threadDelay 100000 alice ##> "/g team" @@ -2722,7 +2722,8 @@ testGroupLink = alice <## "Recent history: off" alice ##> "/create link #team" - gLink <- getGroupLink alice "team" GRMember True + gLink <- getGroupLink_ alice "team" GRMember True + alice ("/c " <> gLink) bob <## "connection request sent!" alice <## "bob (Bob): accepting request to join group #team..." @@ -2748,7 +2749,8 @@ testGroupLink = -- user address doesn't interfere alice ##> "/ad" - cLink <- getContactLink alice True + cLink <- getContactLink_ alice True + alice ("/c " <> cLink) alice <#? cath alice ##> "/ac cath" @@ -2990,7 +2992,7 @@ testGroupLinkMemberRole = bob <## "#team: you joined the group" ] - threadDelay 100000 + threadDelay 250000 alice ##> "/ms team" alice @@ -8780,11 +8782,12 @@ testSupportPreferenceChannel ps = testConnectChannelCLI :: HasCallStack => TestParams -> IO () testConnectChannelCLI ps = - withNewTestChat ps "alice" aliceProfile $ \alice -> - withNewTestChatOpts ps relayTestOpts "bob" bobProfile $ \bob -> - withNewTestChatOpts ps relayTestOpts "cath" cathProfile $ \cath -> - withNewTestChat ps "dan" danProfile $ \dan -> do - (shortLink, _fullLink) <- prepareChannel2Relays "team" alice bob cath + withNewTestChatOpts ps testOptsNoFullLinks "alice" aliceProfile $ \alice -> + withNewTestChatOpts ps relayTestOptsNoFullLinks "bob" bobProfile $ \bob -> + withNewTestChatOpts ps relayTestOptsNoFullLinks "cath" cathProfile $ \cath -> + withNewTestChatOpts ps testOptsNoFullLinks "dan" danProfile $ \dan -> do + (shortLink, fullLink) <- prepareChannel2Relays "team" alice bob cath + fullLink `shouldBe` "" -- public group link "for old clients" is dropped when showFullLinks is off relayNames <- mapM userName [bob, cath] mName <- userName dan mFullName <- showName dan diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 4b82779b0e..c65306474c 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -26,7 +26,7 @@ import qualified Data.Map.Strict as M import Simplex.Chat.Badges (BadgeCredential, BadgeInfo (..), BadgePurchase (..), BadgeRequest (..), BadgeType (..), generateMasterKey, issueBadge, verifyPayment) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatHooks (..), defaultChatHooks, mkStoreCxt) import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) -import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..), currentChatVersion) +import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..)) import Simplex.Chat.Store.Shared (createContact) import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..), profileFromName) import qualified Simplex.Messaging.Crypto as C @@ -61,6 +61,7 @@ chatProfileTests = do it "supporter badge sent to contact connecting via address" testUserBadgeContactAddress describe "user contact link" $ do it "create and connect via contact link" testUserContactLink + it "rotate address ratchet keys" testRotateAddressRatchetKeys it "create address on specified server" testCreateAddressOnServer it "retry connecting via contact link" testRetryConnectingViaContactLink it "add contact link to profile" testProfileLink @@ -616,10 +617,11 @@ testMultiWordProfileNames = testUserContactLink :: HasCallStack => TestParams -> IO () testUserContactLink = - testChat3 aliceProfile bobProfile cathProfile $ + testChatOpts3 testOptsNoFullLinks aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/ad" - cLink <- getContactLink alice True + cLink <- getContactLink_ alice True + alice ("/c " <> cLink) alice <#? bob alice @@@ [("@bob", "Audio/video calls: enabled")] @@ -644,6 +646,29 @@ testUserContactLink = alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice <##> cath +testRotateAddressRatchetKeys :: HasCallStack => TestParams -> IO () +testRotateAddressRatchetKeys = + testChatOpts2 testOptsNoFullLinks aliceProfile bobProfile $ \alice bob -> do + alice ##> "/ad" + sLink1 <- getContactLink_ alice True + alice ##> ("/_connect plan 1 " <> sLink1) + alice <## "contact address: own address" + alice ##> "/_rotate_address_keys 1" + sLink2 <- getContactLink_ alice False + alice <## "auto_accept off" + -- rotated address keeps the same identity (still recognized as own address) + alice ##> ("/_connect plan 1 " <> sLink2) + alice <## "contact address: own address" + -- and it still works for connecting + bob ##> ("/c " <> sLink2) + alice <#? bob + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + testCreateAddressOnServer :: HasCallStack => TestParams -> IO () testCreateAddressOnServer ps = testChat aliceProfile test ps where @@ -1428,6 +1453,7 @@ testPlanAddressConnecting ps = do threadDelay 500000 bob <## "subscribed 1 connections on server localhost" bob <## "alice (Alice): contact is connected" + threadDelay 100000 bob @@@ [("@alice", "Audio/video calls: enabled")] bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: known contact alice" @@ -3040,16 +3066,17 @@ testSetUITheme = a <## "you've shared main profile with this contact" a <## "connection not verified, use /code command to see security code" a <## "quantum resistant end-to-end encryption" - a <## ("peer chat protocol version range: (Version 1, " <> show currentChatVersion <> ")") + a <## currentChatVRangeInfo groupInfo a = do a <## "group ID: 1" a <## "current members: 1" testShortLinkInvitation :: HasCallStack => TestParams -> IO () testShortLinkInvitation = - testChat2 aliceProfile bobProfile $ \alice bob -> do + testChatOpts2 testOptsNoFullLinks aliceProfile bobProfile $ \alice bob -> do alice ##> "/c" - (inv, _) <- getInvitations alice + inv <- getInvitation_ alice + alice ("/c " <> inv) bob <## "confirmation sent!" concurrently_ diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index df2bff8ab6..dfcf76f761 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -127,9 +127,9 @@ versionTestMatrix2 runTest = do it "prev" $ runTestCfg2 testCfgVPrev testCfgVPrev (runTest False True) it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev (runTest False True) it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg (runTest False True) - it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest False False) - it "old to curr" $ runTestCfg2 testCfg testCfgV1 (runTest False True) - it "curr to old" $ runTestCfg2 testCfgV1 testCfg (runTest False False) + it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest True False) + it "old to curr" $ runTestCfg2 testCfg testCfgV1 (runTest True True) + it "curr to old" $ runTestCfg2 testCfgV1 testCfg (runTest True False) versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith TestParams versionTestMatrix3 runTest = do @@ -560,6 +560,12 @@ dropPartialReceipt_ msg = case splitAt 2 msg of ("% ", text) -> Just text _ -> Nothing +getForOldClientsLine :: HasCallStack => TestCC -> String -> IO String +getForOldClientsLine cc prefix = + timeout 500000 (getTermLine cc) >>= \case + Just line -> dropLinePrefix prefix line + Nothing -> pure "" + getInvitation :: HasCallStack => TestCC -> IO String getInvitation cc = do (_, fullInv) <- getInvitations cc @@ -592,8 +598,7 @@ getContactLink cc created = do getContactLinks :: HasCallStack => TestCC -> Bool -> IO (String, String) getContactLinks cc created = do shortLink <- getContactLink_ cc created - line <- getTermLine' (Just "full contact link line") cc - fullLink <- dropLinePrefix "The contact link for old clients: " line + fullLink <- getForOldClientsLine cc "The contact link for old clients: " pure (shortLink, fullLink) getContactLinkNoShortLink :: HasCallStack => TestCC -> Bool -> IO String @@ -624,8 +629,7 @@ getGroupLink cc gName mRole created = do getGroupLinks :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO (String, String) getGroupLinks cc gName mRole created = do shortLink <- getGroupLink_ cc gName mRole created - line <- getTermLine' (Just "full group link line") cc - fullLink <- dropLinePrefix "The group link for old clients: " line + fullLink <- getForOldClientsLine cc "The group link for old clients: " pure (shortLink, fullLink) getGroupLinkNoShortLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 798ab3c4d7..63dbea549f 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -129,70 +129,71 @@ shortLinkDataTests = describe "Short link data encoding/decoding" $ do { direct = True, owners = [], relays = [], - userData = encodeShortLinkData (value :: String) + userData = encodeShortLinkData (value :: String), + ratchetKeys = Nothing } decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do it "x.msg.new simple text" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XMsgNew (mcSimple (MCText "hello")) it "x.msg.new simple text - timed message TTL" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}" #==# XMsgNew ((mcSimple (MCText "hello")) {ttl = Just 3600}) it "x.msg.new simple text - live message" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}" #==# XMsgNew ((mcSimple (MCText "hello")) {live = Just True}) it "x.msg.new simple link" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}" #==# XMsgNew (mcSimple (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing})) it "x.msg.new simple image" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" #==# XMsgNew (mcSimple (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=")) it "x.msg.new simple image with text" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" #==# XMsgNew (mcSimple (MCImage "here's an image" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=")) it "x.msg.new chat message" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-19\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"9-19\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello"))) it "x.msg.new quote" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcQuote quotedMsg (MCText "hello to you too"))) it "x.msg.new quote - timed message TTL" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew ((mcQuote quotedMsg (MCText "hello to you too")) {ttl = Just 3600})) it "x.msg.new quote - live message" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew ((mcQuote quotedMsg (MCText "hello to you too")) {live = Just True})) it "x.msg.new forward" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ mcForward (MCText "hello")) it "x.msg.new forward - timed message TTL" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {ttl = Just 3600}) it "x.msg.new forward - live message" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {live = Just True}) it "x.msg.new simple text with file" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XMsgNew ((mcSimple (MCText "hello")) {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.new simple file with file" $ - "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}" + "{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}" #==# XMsgNew ((mcSimple (MCFile "")) {file = Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.new quote with file" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") @@ -201,134 +202,134 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}} ) it "x.msg.new report" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"reason\":\"spam\",\"type\":\"report\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"reason\":\"spam\",\"type\":\"report\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcQuote quotedMsg (MCReport "" RRSpam))) it "x.msg.new forward with file" $ - "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"forward\":true}}" + "{\"v\":\"9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"},\"forward\":true}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ (mcForward (MCText "hello")) {file = Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}}) it "x.msg.update" $ - "{\"v\":\"1\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"9\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") [] Nothing Nothing Nothing Nothing it "x.msg.del" $ - "{\"v\":\"1\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}" #==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing Nothing False it "x.msg.deleted" $ - "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}" + "{\"v\":\"9\",\"event\":\"x.msg.deleted\",\"params\":{}}" #==# XMsgDeleted it "x.file" $ - "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"9\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing} it "x.file without file invitation" $ - "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"9\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing} it "x.file.acpt" $ - "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" + "{\"v\":\"9\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ - "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"9\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" it "x.file.acpt.inv" $ - "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" + "{\"v\":\"9\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") Nothing "photo.jpg" it "x.file.cancel" $ - "{\"v\":\"1\",\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}" #==# XFileCancel (SharedMsgId "\1\2\3\4") it "x.info" $ - "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"9\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XInfo testProfile it "x.info with empty full name" $ - "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"9\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XInfo Profile {displayName = "alice", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing} it "x.contact with xContactId" $ - "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"9\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing Nothing it "x.contact without XContactId" $ - "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"9\",\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile Nothing Nothing Nothing it "x.contact with content null" $ - "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"9\",\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing Nothing Nothing it "x.contact with content" $ - "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"msgId\":\"AQIDBA==\",\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"9\",\"event\":\"x.contact\",\"params\":{\"msgId\":\"AQIDBA==\",\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing Nothing (Just (SharedMsgId "\1\2\3\4", MCText {text = "hello"})) it "x.grp.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing} it "x.grp.inv with group link id" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" + "{\"v\":\"9\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} it "x.grp.acpt without incognito profile" $ - "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpAcpt (MemberId "\1\2\3\4") it "x.grp.mem.new" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"9-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"9-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} Nothing it "x.grp.mem.intro with member restrictions" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} (Just MemberRestrictions {restriction = MRSBlocked}) it "x.grp.mem.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.inv w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.fwd" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"9-19\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile, memberKey = Nothing} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile it "x.grp.mem.con" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpMemCon (MemberId "\1\2\3\4") it "x.grp.mem.con.all" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpMemConAll (MemberId "\1\2\3\4") it "x.grp.mem.del" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpMemDel (MemberId "\1\2\3\4") False Nothing it "x.grp.leave" $ - "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}" + "{\"v\":\"9\",\"event\":\"x.grp.leave\",\"params\":{}}" ==# XGrpLeave it "x.grp.del" $ - "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}" + "{\"v\":\"9\",\"event\":\"x.grp.del\",\"params\":{}}" ==# XGrpDel it "x.grp.direct.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"9\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XGrpDirectInv testConnReq (Just $ MCText "hello") Nothing it "x.grp.direct.inv without content" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"9\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XGrpDirectInv testConnReq Nothing Nothing -- it "x.grp.msg.forward" - -- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" + -- $ "{\"v\":\"9\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"9\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" -- #==# XGrpMsgForward -- (MemberId "\1\2\3\4") -- (ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (mcSimple (MCText "hello")))) -- (systemToUTCTime $ MkSystemTime 1 1) it "x.info.probe" $ - "{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}" #==# XInfoProbe (Probe "\1\2\3\4") it "x.info.probe.check" $ - "{\"v\":\"1\",\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}" #==# XInfoProbeCheck (ProbeHash "\1\2\3\4") it "x.info.probe.ok" $ - "{\"v\":\"1\",\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}" + "{\"v\":\"9\",\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}" #==# XInfoProbeOk (Probe "\1\2\3\4") it "x.ok" $ - "{\"v\":\"1\",\"event\":\"x.ok\",\"params\":{}}" + "{\"v\":\"9\",\"event\":\"x.ok\",\"params\":{}}" ==# XOk