Merge branch 'master' into ep/p2p-group-signing

This commit is contained in:
Evgeny Poberezkin committed 2026-08-01 21:24:34 +01:00
commit 39957589e0
58 files changed
+1299 -261

No files matched your search

+2 -1
View File
@@ -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)
+28 -1
View File
@@ -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.
@@ -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<ChatPeerType> {
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.
@@ -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()
@@ -90,6 +90,7 @@ mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} =
optFilesFolder = Nothing,
optTempDirectory = Nothing,
showReactions = False,
showFullLinks = False,
allowInstantFiles = True,
autoAcceptFileSize = 0,
muteNotifications = True,
@@ -245,6 +245,7 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName, clientService} =
optFilesFolder = Nothing,
optTempDirectory = Nothing,
showReactions = False,
showFullLinks = False,
allowInstantFiles = True,
autoAcceptFileSize = 0,
muteNotifications = True,
+67 -7
View File
@@ -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 <userId>
/_address <userId>[ 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 <userId> <json(settings)>
/_address_settings <userId>[ pq_ratchet=on|off] <json(settings)>
```
```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**:
@@ -1743,6 +1749,7 @@ GroupDeletedUser: User deleted group.
- user: [User](./TYPES.md#user)
- groupInfo: [GroupInfo](./TYPES.md#groupinfo)
- msgSigned: bool
- localDeletion: bool
ChatCmdError: Command error (only used in WebSockets API).
- type: "chatCmdError"
@@ -2118,6 +2125,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 <userId> <requestId> <json(responseData)>
```
```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 +2183,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**:
+38
View File
@@ -68,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)
@@ -755,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).
+42
View File
@@ -9,6 +9,7 @@ This file is generated automatically.
- [AddressSettings](#addresssettings)
- [AgentCryptoError](#agentcryptoerror)
- [AgentErrorType](#agenterrortype)
- [AgentServiceError](#agentserviceerror)
- [AutoAccept](#autoaccept)
- [BadgeInfo](#badgeinfo)
- [BadgeProof](#badgeproof)
@@ -208,6 +209,7 @@ This file is generated automatically.
- [UserContact](#usercontact)
- [UserContactLink](#usercontactlink)
- [UserContactRequest](#usercontactrequest)
- [UserContactRequestRef](#usercontactrequestref)
- [UserInfo](#userinfo)
- [UserProfileUpdateSummary](#userprofileupdatesummary)
- [UserPwdHash](#userpwdhash)
@@ -362,6 +364,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
@@ -1425,6 +1450,7 @@ Search:
**Enum type**:
- "human"
- "bot"
- "business"
---
@@ -1784,6 +1810,7 @@ Error:
- chatTs: UTCTime?
- preparedContact: [PreparedContact](#preparedcontact)?
- contactRequestId: int64?
- contactRequest: [UserContactRequestRef](#usercontactrequestref)?
- contactGroupMemberId: int64?
- contactGrpInvSent: bool
- groupDirectInv: [GroupDirectInvitation](#groupdirectinvitation)?
@@ -1843,6 +1870,7 @@ ContactViaAddress:
- "active"
- "deleted"
- "deletedByUser"
- "rejected"
---
@@ -3617,6 +3645,10 @@ A_QUEUE:
- type: "A_QUEUE"
- queueErr: string
A_SERVICE:
- type: "A_SERVICE"
- serviceError: [AgentServiceError](#agentserviceerror)
---
@@ -4429,6 +4461,16 @@ Handshake:
- pqSupport: bool
- welcomeSharedMsgId: string?
- requestSharedMsgId: string?
- rejectionSupported: bool
---
## UserContactRequestRef
**Record type**:
- contactRequestId: int64
- rejectionSupported: bool
---
+10 -3
View File
@@ -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",
+8
View File
@@ -145,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 - \
@@ -183,6 +190,7 @@ undocumentedEvents =
"CEvtContactPQEnabled",
"CEvtContactRatchetSync",
"CEvtContactRequestAlreadyAccepted",
"CEvtContactRequestRejected",
"CEvtContactSwitch",
"CEvtCustomChatEvent",
"CEvtGroupMemberRatchetSync",
+2
View File
@@ -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",
+4 -1
View File
@@ -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, "", [], "", ""),
@@ -224,7 +226,7 @@ chatTypesDocsData =
(sti @BadgeType, STEnum, "BT", ["BTUnknown"], "", ""),
(sti @ChatFeature, STEnum, "CF", [], "", ""),
(sti @ChatItemDeletion, STRecord, "", [], "", "Message deletion result."),
(sti @ChatPeerType, STEnum, "CPT", [], "", ""),
(sti @ChatPeerType, STEnum, "CPT", ["CPTUnknown"], "", ""),
(sti @ChatRef, STRecord, "", [], Param "chatType" <> Param "chatId" <> Optional "" (Param "$0") "chatScope", "Used in API commands. Chat scope can only be passed with groups."),
(sti @ChatSettings, STRecord, "", [], "", ""),
(sti @ChatStats, STRecord, "", [], "", ""),
@@ -437,6 +439,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
+1 -1
View File
@@ -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
@@ -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' : '')
}
}
@@ -50,6 +50,8 @@ export type ChatEvent =
| CEvt.HostConnected
| CEvt.HostDisconnected
| CEvt.SubscriptionStatus
| CEvt.ServiceRequest
| CEvt.ServiceReplySent
| CEvt.MessageError
| CEvt.ChatError
| CEvt.ChatErrors
@@ -102,6 +104,8 @@ export namespace CEvt {
| "hostConnected"
| "hostDisconnected"
| "subscriptionStatus"
| "serviceRequest"
| "serviceReplySent"
| "messageError"
| "chatError"
| "chatErrors"
@@ -454,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
@@ -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"
@@ -234,6 +236,7 @@ export namespace CR {
user: T.User
groupInfo: T.GroupInfo
msgSigned: boolean
localDeletion: boolean
}
export interface GroupLink extends Interface {
@@ -409,6 +412,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
@@ -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
}
@@ -1644,6 +1680,7 @@ export namespace ChatListQuery {
export enum ChatPeerType {
Human = "human",
Bot = "bot",
Business = "business",
}
// Used in API commands. Chat scope can only be passed with groups.
@@ -2021,6 +2058,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 +2131,7 @@ export enum ContactStatus {
Active = "active",
Deleted = "deleted",
DeletedByUser = "deletedByUser",
Rejected = "rejected",
}
export type ContactUserPref = ContactUserPref.Contact | ContactUserPref.User
@@ -3963,6 +4002,7 @@ export type SMPAgentError =
| SMPAgentError.A_CRYPTO
| SMPAgentError.A_DUPLICATE
| SMPAgentError.A_QUEUE
| SMPAgentError.A_SERVICE
export namespace SMPAgentError {
export type Tag =
@@ -3973,6 +4013,7 @@ export namespace SMPAgentError {
| "A_CRYPTO"
| "A_DUPLICATE"
| "A_QUEUE"
| "A_SERVICE"
interface Interface {
type: Tag
@@ -4010,6 +4051,11 @@ export namespace SMPAgentError {
type: "A_QUEUE"
queueErr: string
}
export interface A_SERVICE extends Interface {
type: "A_SERVICE"
serviceError: AgentServiceError
}
}
export interface SecurityCode {
@@ -5080,6 +5126,12 @@ export interface UserContactRequest {
pqSupport: boolean
welcomeSharedMsgId?: string
requestSharedMsgId?: string
rejectionSupported: boolean
}
export interface UserContactRequestRef {
contactRequestId: number // int64
rejectionSupported: boolean
}
export interface UserInfo {
@@ -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
@@ -303,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"
@@ -364,12 +375,14 @@ ChatEvent = (
| 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", "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):
@@ -656,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]]],
@@ -98,6 +98,7 @@ class GroupDeletedUser(TypedDict):
user: "T.User"
groupInfo: "T.GroupInfo"
msgSigned: bool
localDeletion: bool
class GroupLink(TypedDict):
type: Literal["groupLink"]
@@ -247,6 +248,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 +358,7 @@ ChatResponse = (
| SentConfirmation
| SentGroupInvitation
| SentInvitation
| ServiceReplyAccepted
| SndFileCancelled
| UserAcceptedGroupSent
| UserContactLink
@@ -365,4 +372,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"]
@@ -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
@@ -1149,7 +1175,7 @@ ChatListQuery = ChatListQuery_filters | ChatListQuery_search
ChatListQuery_Tag = Literal["filters", "search"]
ChatPeerType = Literal["human", "bot"]
ChatPeerType = Literal["human", "bot", "business"]
# Used in API commands. Chat scope can only be passed with groups.
@@ -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"]
@@ -2788,6 +2815,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
@@ -2796,9 +2827,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
@@ -3564,6 +3596,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"
+270
View File
@@ -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=<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 <key>` 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 <pubStr>` matching
the sender's key.
+1 -1
View File
@@ -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";
+3 -1
View File
@@ -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
Simplex.Chat.Store.Postgres.Migrations.M20260727_member_key_sent
else
exposed-modules:
@@ -322,6 +323,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
Simplex.Chat.Store.SQLite.Migrations.M20260727_member_key_sent
other-modules:
Paths_simplex_chat
+5 -2
View File
@@ -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,
+2 -2
View File
@@ -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
+19 -9
View File
@@ -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]}
@@ -941,6 +948,9 @@ data ChatEvent
| 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}
+2 -2
View File
@@ -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
+125 -84
View File
@@ -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")
@@ -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
@@ -2680,7 +2716,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
@@ -2691,9 +2727,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 {publicGroupKeys, memberPrivKey}
publicGroupKeys = Just PublicGroupKeys {publicGroupId = B64UrlByteString entityId, groupRootKey = GRKPrivate rootPrivKey}
setupLink gInfo = do
@@ -2760,7 +2796,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
@@ -3247,9 +3283,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
@@ -3289,7 +3325,7 @@ processChatCommand cxt nm = \case
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)
@@ -3759,7 +3795,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
@@ -3791,8 +3827,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
@@ -3832,7 +3868,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
@@ -3853,15 +3889,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)
@@ -3950,8 +3985,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
@@ -3992,16 +4029,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'}}
@@ -4220,7 +4258,7 @@ 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 _ -> do
@@ -4319,7 +4357,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
@@ -4373,7 +4411,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
@@ -4433,13 +4471,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
@@ -4478,14 +4516,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
@@ -4552,15 +4590,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
@@ -4582,8 +4619,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
@@ -4609,10 +4646,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.
@@ -4663,7 +4700,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
@@ -5012,7 +5049,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
@@ -5294,7 +5331,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
@@ -5412,8 +5449,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),
@@ -5486,7 +5524,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),
@@ -5705,19 +5745,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),
+15 -15
View File
@@ -1461,7 +1461,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 ()
@@ -1549,28 +1549,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 =
@@ -2896,7 +2894,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
@@ -2910,7 +2908,7 @@ 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
@@ -3018,9 +3016,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' =
@@ -3274,7 +3274,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/v8AQ1p0AAAAAADoBRX5nUkpSbiLine truncated
+30 -16
View File
@@ -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
@@ -519,7 +522,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
@@ -746,7 +752,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
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
@@ -1194,7 +1200,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
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
@@ -1206,14 +1212,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 _ -> 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
@@ -1361,16 +1367,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 memberKey_ xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p memberKey_ xContactId_ welcomeMsgId_ requestMsg_ pqSupport
XContact p memberKey_ xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p memberKey_ 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 Nothing pqSupport
XInfo p _ -> profileContactRequest invId chatVRange p Nothing 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
@@ -1432,8 +1446,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
-- TODO add debugging output
_ -> pure ()
where
profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe MemberKey -> Maybe XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> CM ()
profileContactRequest invId chatVRange p@Profile {displayName} memberKey_ xContactId_ welcomeMsgId_ requestMsg_ reqPQSup = do
profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe MemberKey -> Maybe XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> Bool -> CM ()
profileContactRequest invId chatVRange p@Profile {displayName} memberKey_ xContactId_ welcomeMsgId_ requestMsg_ reqPQSup rejectionSupported = do
(ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId
let v = maxVersion chatVRange
case gLinkInfo_ of
@@ -1443,7 +1457,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
@@ -3210,7 +3224,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
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
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 ()
@@ -4420,7 +4434,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
@@ -4450,15 +4464,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
+1 -1
View File
@@ -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
+1
View File
@@ -276,6 +276,7 @@ mobileChatOpts dbOptions =
optFilesFolder = Nothing,
optTempDirectory = Nothing,
showReactions = False,
showFullLinks = False,
allowInstantFiles = True,
autoAcceptFileSize = 0,
muteNotifications = True,
+7
View File
@@ -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,
+2 -2
View File
@@ -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
+5 -3
View File
@@ -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
+9 -6
View File
@@ -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
+6 -3
View File
@@ -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 = ?
|]
+1 -1
View File
@@ -3306,7 +3306,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
+1 -1
View File
@@ -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,
@@ -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.Chat.Store.Postgres.Migrations.M20260727_member_key_sent
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
@@ -91,6 +92,7 @@ schemaMigrations =
("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),
("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection),
("20260727_member_key_sent", m20260727_member_key_sent, Just down_m20260727_member_key_sent)
]
@@ -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;
|]
@@ -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
);
@@ -995,7 +996,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,
@@ -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.Chat.Store.SQLite.Migrations.M20260727_member_key_sent
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
@@ -337,6 +338,7 @@ schemaMigrations =
("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),
("20260723_contact_request_rejection", m20260723_contact_request_rejection, Just down_m20260723_contact_request_rejection),
("20260727_member_key_sent", m20260727_member_key_sent, Just down_m20260727_member_key_sent)
]
@@ -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;
|]
@@ -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:
@@ -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,
@@ -5653,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,
@@ -5670,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,
@@ -430,6 +430,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
+7 -6
View File
@@ -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 =
+54 -7
View File
@@ -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)
@@ -713,7 +724,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
@@ -728,13 +739,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 =
@@ -999,6 +1013,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}
@@ -1841,6 +1875,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
@@ -2357,6 +2402,8 @@ $(JQ.deriveJSON defaultJSON ''FileTransferMeta)
$(JQ.deriveJSON defaultJSON ''PreparedContact)
$(JQ.deriveJSON defaultJSON ''UserContactRequestRef)
$(JQ.deriveJSON defaultJSON ''GroupDirectInvitation)
$(JQ.deriveJSON defaultJSON ''LocalFileMeta)
+30 -21
View File
@@ -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]
@@ -462,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
@@ -1038,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,
@@ -1047,7 +1056,7 @@ viewConnReqInvitation (CCLink cReq shortLink) =
"and ask them to connect: " <> highlight' "/c <invitation_link_above>"
]
<>
if isJust shortLink
if showFullLinks && isJust shortLink
then
[ "The invitation link for old clients:",
plain cReqStr
@@ -1110,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,
@@ -1121,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
@@ -1171,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
@@ -1183,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
@@ -2115,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]
@@ -2128,7 +2137,7 @@ viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {
""
]
<>
if isJust shortLink
if showFullLinks && isJust shortLink
then
[ "The invitation link for old clients:",
plain cReqStr
+8 -1
View File
@@ -121,6 +121,7 @@ testOpts =
optFilesFolder = Nothing,
optTempDirectory = Nothing,
showReactions = True,
showFullLinks = True,
allowInstantFiles = True,
autoAcceptFileSize = 0,
muteNotifications = True,
@@ -172,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}}}
@@ -184,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
}
+78 -1
View File
@@ -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 $
+12 -9
View File
@@ -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 <// 100000 -- the "group link for old clients" line is dropped when showFullLinks is off
bob ##> ("/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 <// 100000 -- the "contact link for old clients" line is dropped when showFullLinks is off
cath ##> ("/c " <> cLink)
alice <#? cath
alice ##> "/ac cath"
@@ -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
+31 -4
View File
@@ -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 <// 100000 -- the "contact link for old clients" line is dropped when showFullLinks is off
bob ##> ("/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"
@@ -3047,9 +3073,10 @@ testSetUITheme =
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 <// 100000 -- the "invitation link for old clients" line is dropped when showFullLinks is off
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
concurrently_
+8 -4
View File
@@ -570,6 +570,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
@@ -602,8 +608,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
@@ -634,8 +639,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
+2 -1
View File
@@ -129,7 +129,8 @@ shortLinkDataTests = describe "Short link data encoding/decoding" $ do
{ direct = True,
owners = [],
relays = [],
userData = encodeShortLinkData (value :: String)
userData = encodeShortLinkData (value :: String),
ratchetKeys = Nothing
}
decodeChatMessageTest :: Spec