mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-02 22:11:54 +00:00
refactor(names): agent resolution + one error type
Adopt the simplexmq names rework (PR #7045): name resolution is now owned by the agent (resolveSimplexName picks a names-role server), so the chat-side iteration is removed - delete ResolveError, iterateResolvers, resolveOnUserServers, enabledSMPServersForUser and resolveErrorToChatError. One error type: resolver/agent failures flow through ChatErrorAgent; remove the CEvtSimplexName* events, SimplexNameVerifyFailReason, SimplexNameConflictEntity and CESimplexNameResolverUnavailable. APIVerifySimplexName returns CRSimplexNameVerified (verified::Bool), mirroring CRConnectionVerified. connectPlan handles the name target directly; updateProfile WithConflict aliases collapsed into the plain functions. Add the per-operator "names" SMP server role (migration 20260612_smp_role_names, official operator on by default) feeding ServerRoles.names -> UserServers.nameSrvs. Bump simplexmq pin to ce69adfd and regenerate sha256map.nix.
This commit is contained in:
@@ -1862,7 +1862,7 @@ ChatCmdError: Command error (only used in WebSockets API).
|
||||
|
||||
### APIVerifySimplexName
|
||||
|
||||
Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Synchronously returns `CRCmdOk`; the verification outcome is delivered asynchronously via [CEvtSimplexNameVerified](./EVENTS.md#cevtsimplexnameverified) or [CEvtSimplexNameVerifyFailed](./EVENTS.md#cevtsimplexnameverifyfailed).
|
||||
Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Returns `CRSimplexNameVerified` with a boolean `verified` (a match also writes the verification timestamp); resolver / agent failures are reported as `CRChatCmdError`.
|
||||
|
||||
*Network usage*: interactive.
|
||||
|
||||
@@ -1885,9 +1885,12 @@ Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim a
|
||||
|
||||
**Responses**:
|
||||
|
||||
CmdOk: Ok.
|
||||
- type: "cmdOk"
|
||||
- user_: [User](./TYPES.md#user)?
|
||||
SimplexNameVerified: Result of SimpleX name verification (`verified`: whether the RSLV-resolved link matches the peer's stored link).
|
||||
- type: "simplexNameVerified"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatRef: [ChatRef](./TYPES.md#chatref)
|
||||
- simplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)
|
||||
- verified: bool
|
||||
|
||||
ChatCmdError: Command error (only used in WebSockets API).
|
||||
- type: "chatCmdError"
|
||||
|
||||
@@ -10,10 +10,6 @@ This file is generated automatically.
|
||||
- [ContactDeletedByContact](#contactdeletedbycontact)
|
||||
- [ReceivedContactRequest](#receivedcontactrequest)
|
||||
- [NewMemberContactReceivedInv](#newmembercontactreceivedinv)
|
||||
- [SimplexNameConflict](#simplexnameconflict)
|
||||
- [SimplexNameVerified](#simplexnameverified)
|
||||
- [SimplexNameVerifyFailed](#simplexnameverifyfailed)
|
||||
- [SimplexNameUnverified](#simplexnameunverified)
|
||||
- [ContactSndReady](#contactsndready)
|
||||
|
||||
[Message events](#message-events)
|
||||
@@ -164,62 +160,6 @@ This event only needs to be processed to associate contact with group, the conne
|
||||
---
|
||||
|
||||
|
||||
### SimplexNameConflict
|
||||
|
||||
A peer's profile update claimed a SimpleX name (`#name.simplex` / `@name.simplex`) that was already held locally by another contact or group. The displaced row's `simplex_name` is set to NULL and the claim moves to the newer row; this event surfaces the displacement so the bot can warn the user about a possible impersonation.
|
||||
|
||||
**Record type**:
|
||||
- type: "simplexNameConflict"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- simplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)
|
||||
- entity: [SimplexNameConflictEntity](./TYPES.md#simplexnameconflictentity)
|
||||
- claimedBy: string
|
||||
- displacedFrom: string
|
||||
|
||||
---
|
||||
|
||||
|
||||
### SimplexNameVerified
|
||||
|
||||
RSLV verification of a contact's or group's SimpleX name claim succeeded: the resolved link matches the peer's stored connection link and `simplex_name_verified_at` has been written. Emitted asynchronously after [APIVerifySimplexName](./COMMANDS.md#apiverifysimplexname).
|
||||
|
||||
**Record type**:
|
||||
- type: "simplexNameVerified"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatRef: [ChatRef](./TYPES.md#chatref)
|
||||
- simplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)
|
||||
- verifiedAt: UTCTime
|
||||
|
||||
---
|
||||
|
||||
|
||||
### SimplexNameVerifyFailed
|
||||
|
||||
RSLV verification of a SimpleX name claim did not succeed. `reason` indicates the cause: `SNVFLinkMismatch` (resolved link does not match stored link), `SNVFNameNotRegistered` (no on-chain record exists), or `SNVFResolverError` (transport / proxy failure, with the agent error inline). The `simplex_name` claim is **not** cleared. Emitted asynchronously after [APIVerifySimplexName](./COMMANDS.md#apiverifysimplexname).
|
||||
|
||||
**Record type**:
|
||||
- type: "simplexNameVerifyFailed"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatRef: [ChatRef](./TYPES.md#chatref)
|
||||
- simplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)
|
||||
- reason: [SimplexNameVerifyFailReason](./TYPES.md#simplexnameverifyfailreason)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### SimplexNameUnverified
|
||||
|
||||
A peer's incoming profile update (XInfo / XGrpInfo) carries a SimpleX name claim that the user has not yet verified — i.e. `simplex_name_verified_at` is NULL. Bots / UI may surface an unverified indicator next to the contact / group name; the user can clear it by invoking [APIVerifySimplexName](./COMMANDS.md#apiverifysimplexname).
|
||||
|
||||
**Record type**:
|
||||
- type: "simplexNameUnverified"
|
||||
- user: [User](./TYPES.md#user)
|
||||
- chatRef: [ChatRef](./TYPES.md#chatref)
|
||||
- simplexName: [SimplexNameInfo](./TYPES.md#simplexnameinfo)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### ContactSndReady
|
||||
|
||||
Connecting via 1-time invitation or after accepting contact request.
|
||||
|
||||
+29
-32
@@ -134,6 +134,7 @@ This file is generated automatically.
|
||||
- [MsgReaction](#msgreaction)
|
||||
- [MsgReceiptStatus](#msgreceiptstatus)
|
||||
- [MsgSigStatus](#msgsigstatus)
|
||||
- [NameErrorType](#nameerrortype)
|
||||
- [NetworkError](#networkerror)
|
||||
- [NewUser](#newuser)
|
||||
- [NoteFolder](#notefolder)
|
||||
@@ -168,11 +169,9 @@ This file is generated automatically.
|
||||
- [SecurityCode](#securitycode)
|
||||
- [SimplePreference](#simplepreference)
|
||||
- [SimplexLinkType](#simplexlinktype)
|
||||
- [SimplexNameConflictEntity](#simplexnameconflictentity)
|
||||
- [SimplexNameDomain](#simplexnamedomain)
|
||||
- [SimplexNameInfo](#simplexnameinfo)
|
||||
- [SimplexNameType](#simplexnametype)
|
||||
- [SimplexNameVerifyFailReason](#simplexnameverifyfailreason)
|
||||
- [SimplexTLD](#simplextld)
|
||||
- [SndCIStatusProgress](#sndcistatusprogress)
|
||||
- [SndConnEvent](#sndconnevent)
|
||||
@@ -310,6 +309,10 @@ FILE:
|
||||
- type: "FILE"
|
||||
- fileErr: [FileErrorType](#fileerrortype)
|
||||
|
||||
NAME:
|
||||
- type: "NAME"
|
||||
- nameErr: [NameErrorType](#nameerrortype)
|
||||
|
||||
PROXY:
|
||||
- type: "PROXY"
|
||||
- proxyServer: string
|
||||
@@ -1066,10 +1069,6 @@ SimplexNameUnprepared:
|
||||
- type: "simplexNameUnprepared"
|
||||
- simplexName: [SimplexNameInfo](#simplexnameinfo)
|
||||
|
||||
SimplexNameResolverUnavailable:
|
||||
- type: "simplexNameResolverUnavailable"
|
||||
- simplexName: [SimplexNameInfo](#simplexnameinfo)
|
||||
|
||||
UnsupportedConnReq:
|
||||
- type: "unsupportedConnReq"
|
||||
|
||||
@@ -1961,6 +1960,10 @@ EXPIRED:
|
||||
INTERNAL:
|
||||
- type: "INTERNAL"
|
||||
|
||||
NAME:
|
||||
- type: "NAME"
|
||||
- nameErr: [NameErrorType](#nameerrortype)
|
||||
|
||||
DUPLICATE_:
|
||||
- type: "DUPLICATE_"
|
||||
|
||||
@@ -2897,6 +2900,26 @@ Unknown:
|
||||
- "signedNoKey"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## NameErrorType
|
||||
|
||||
**Discriminated union type**:
|
||||
|
||||
NO_RESOLVER:
|
||||
- type: "NO_RESOLVER"
|
||||
|
||||
NO_NAME:
|
||||
- type: "NO_NAME"
|
||||
|
||||
NO_SERVERS:
|
||||
- type: "NO_SERVERS"
|
||||
|
||||
RESOLVER:
|
||||
- type: "RESOLVER"
|
||||
- resolverErr: string
|
||||
|
||||
|
||||
---
|
||||
|
||||
## NetworkError
|
||||
@@ -3509,15 +3532,6 @@ A_QUEUE:
|
||||
- "relay"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexNameConflictEntity
|
||||
|
||||
**Enum type**:
|
||||
- "contact"
|
||||
- "group"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexNameDomain
|
||||
@@ -3546,23 +3560,6 @@ A_QUEUE:
|
||||
- "contact"
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexNameVerifyFailReason
|
||||
|
||||
**Discriminated union type**:
|
||||
|
||||
LinkMismatch:
|
||||
- type: "linkMismatch"
|
||||
|
||||
NameNotRegistered:
|
||||
- type: "nameNotRegistered"
|
||||
|
||||
ResolverError:
|
||||
- type: "resolverError"
|
||||
- agentError: [AgentErrorType](#agenterrortype)
|
||||
|
||||
|
||||
---
|
||||
|
||||
## SimplexTLD
|
||||
|
||||
@@ -152,9 +152,7 @@ chatCommandsDocsData =
|
||||
("APISetGroupCustomData", [], "Set group custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom #" <> Param "groupId" <> Optional "" (" " <> Json "$0") "customData"),
|
||||
("APISetContactCustomData", [], "Set contact custom data.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set custom @" <> Param "contactId" <> Optional "" (" " <> Json "$0") "customData"),
|
||||
("APISetUserAutoAcceptMemberContacts", [], "Set auto-accept member contacts.", ["CRCmdOk", "CRChatCmdError"], [], Nothing, "/_set accept member contacts " <> Param "userId" <> " " <> OnOff "onOff"),
|
||||
-- Resolves the chat row's simplex_name claim via RSLV and compares the resolved per-type link to the peer's stored connection link.
|
||||
-- On match emits [CEvtSimplexNameVerified](./EVENTS.md#cevtsimplexnameverified); on mismatch or resolver failure emits [CEvtSimplexNameVerifyFailed](./EVENTS.md#cevtsimplexnameverifyfailed).
|
||||
("APIVerifySimplexName", [], "Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Synchronously returns `CRCmdOk`; the verification outcome is delivered asynchronously via [CEvtSimplexNameVerified](./EVENTS.md#cevtsimplexnameverified) or [CEvtSimplexNameVerifyFailed](./EVENTS.md#cevtsimplexnameverifyfailed).", ["CRCmdOk", "CRChatCmdError"], [], Just UNInteractive, "/_verify simplex name " <> Param "chatRef")
|
||||
("APIVerifySimplexName", [], "Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Returns `CRSimplexNameVerified` with a boolean `verified` (a match also writes the verification timestamp); resolver / agent failures are reported as `CRChatCmdError`.", ["CRSimplexNameVerified", "CRChatCmdError"], [], Just UNInteractive, "/_verify simplex name " <> Param "chatRef")
|
||||
-- ("APIChatItemsRead", [], "Mark items as read.", ["CRItemsReadForChat"], [], Nothing, ""),
|
||||
-- ("APIChatRead", [], "Mark chat as read.", ["CRCmdOk"], [], Nothing, ""),
|
||||
-- ("APIChatUnread", [], "Mark chat as unread.", ["CRCmdOk"], [], Nothing, ""),
|
||||
|
||||
@@ -67,10 +67,6 @@ chatEventsDocsData =
|
||||
("CEvtContactDeletedByContact", "Bot user's connection with another contact is deleted (conversation is kept)."),
|
||||
("CEvtReceivedContactRequest", "Contact request received.\n\nThis event is only sent when auto-accept is disabled.\n\nThe request needs to be accepted using [APIAcceptContact](./COMMANDS.md#apiacceptcontact) command"),
|
||||
("CEvtNewMemberContactReceivedInv", "Received invitation to connect directly with a group member.\n\nThis event only needs to be processed to associate contact with group, the connection will proceed automatically."),
|
||||
("CEvtSimplexNameConflict", "A peer's profile update claimed a SimpleX name (`#name.simplex` / `@name.simplex`) that was already held locally by another contact or group. The displaced row's `simplex_name` is set to NULL and the claim moves to the newer row; this event surfaces the displacement so the bot can warn the user about a possible impersonation."),
|
||||
("CEvtSimplexNameVerified", "RSLV verification of a contact's or group's SimpleX name claim succeeded: the resolved link matches the peer's stored connection link and `simplex_name_verified_at` has been written. Emitted asynchronously after [APIVerifySimplexName](./COMMANDS.md#apiverifysimplexname)."),
|
||||
("CEvtSimplexNameVerifyFailed", "RSLV verification of a SimpleX name claim did not succeed. `reason` indicates the cause: `SNVFLinkMismatch` (resolved link does not match stored link), `SNVFNameNotRegistered` (no on-chain record exists), or `SNVFResolverError` (transport / proxy failure, with the agent error inline). The `simplex_name` claim is **not** cleared. Emitted asynchronously after [APIVerifySimplexName](./COMMANDS.md#apiverifysimplexname)."),
|
||||
("CEvtSimplexNameUnverified", "A peer's incoming profile update (XInfo / XGrpInfo) carries a SimpleX name claim that the user has not yet verified — i.e. `simplex_name_verified_at` is NULL. Bots / UI may surface an unverified indicator next to the contact / group name; the user can clear it by invoking [APIVerifySimplexName](./COMMANDS.md#apiverifysimplexname)."),
|
||||
("CEvtContactSndReady", "Connecting via 1-time invitation or after accepting contact request.\n\nAfter this event bot can send messages to this contact.") -- JOINED
|
||||
]
|
||||
),
|
||||
|
||||
@@ -89,6 +89,7 @@ chatResponsesDocsData =
|
||||
("CRSentConfirmation", "Confirmation sent to one-time invitation"),
|
||||
("CRSentGroupInvitation", "Group invitation sent"),
|
||||
("CRSentInvitation", "Invitation sent to contact address"),
|
||||
("CRSimplexNameVerified", "Result of SimpleX name verification (`verified`: whether the RSLV-resolved link matches the peer's stored link)"),
|
||||
("CRSndFileCancelled", "Cancelled sending file"),
|
||||
("CRUserAcceptedGroupSent", "User accepted group invitation"),
|
||||
("CRUserContactLink", "User contact address"),
|
||||
|
||||
@@ -44,7 +44,7 @@ import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Client
|
||||
import Simplex.Messaging.Crypto.File
|
||||
import Simplex.Messaging.Parsers (dropPrefix, fstToLower)
|
||||
import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NetworkError (..), ProxyError (..))
|
||||
import Simplex.Messaging.Protocol (BlockingInfo (..), BlockingReason (..), CommandError (..), ErrorType (..), NameErrorType (..), NetworkError (..), ProxyError (..))
|
||||
import Simplex.Messaging.Protocol.Types (ClientNotice (..))
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.RemoteControl.Types
|
||||
@@ -315,6 +315,7 @@ chatTypesDocsData =
|
||||
(sti @MsgReaction, STUnion, "MR", [], "", ""),
|
||||
(sti @MsgReceiptStatus, STEnum, "MR", [], "", ""),
|
||||
(sti @MsgSigStatus, STEnum, "MSS", [], "", ""),
|
||||
(sti @NameErrorType, STUnion, "", [], "", ""),
|
||||
(sti @NetworkError, STUnion, "NE", [], "", ""),
|
||||
(sti @NewUser, STRecord, "", [], "", ""),
|
||||
(sti @NoteFolder, STRecord, "", [], "", ""),
|
||||
@@ -348,11 +349,9 @@ chatTypesDocsData =
|
||||
(sti @SecurityCode, STRecord, "", [], "", ""),
|
||||
(sti @SimplePreference, STRecord, "", [], "", ""),
|
||||
(sti @SimplexLinkType, STEnum, "XL", [], "", ""),
|
||||
(sti @SimplexNameConflictEntity, STEnum, "SNCE", [], "", ""),
|
||||
(sti @SimplexNameDomain, STRecord, "", [], "", ""),
|
||||
(sti @SimplexNameInfo, STRecord, "", [], "", ""),
|
||||
(sti @SimplexNameType, STEnum, "NT", [], "", ""),
|
||||
(sti @SimplexNameVerifyFailReason, STUnion, "SNVF", [], "", ""),
|
||||
(sti @SimplexTLD, STEnum, "TLD", [], "", ""),
|
||||
(sti @SMPAgentError, STUnion, "", [], "", ""),
|
||||
(sti @SndCIStatusProgress, STEnum, "SSP", [], "", ""),
|
||||
@@ -539,6 +538,7 @@ deriving instance Generic MsgFilter
|
||||
deriving instance Generic MsgReaction
|
||||
deriving instance Generic MsgReceiptStatus
|
||||
deriving instance Generic MsgSigStatus
|
||||
deriving instance Generic NameErrorType
|
||||
deriving instance Generic NetworkError
|
||||
deriving instance Generic NewUser
|
||||
deriving instance Generic NoteFolder
|
||||
@@ -570,11 +570,9 @@ deriving instance Generic RelayStatus
|
||||
deriving instance Generic ReportReason
|
||||
deriving instance Generic SecurityCode
|
||||
deriving instance Generic SimplexLinkType
|
||||
deriving instance Generic SimplexNameConflictEntity
|
||||
deriving instance Generic SimplexNameDomain
|
||||
deriving instance Generic SimplexNameInfo
|
||||
deriving instance Generic SimplexNameType
|
||||
deriving instance Generic SimplexNameVerifyFailReason
|
||||
deriving instance Generic SimplexTLD
|
||||
deriving instance Generic SMPAgentError
|
||||
deriving instance Generic SndCIStatusProgress
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: df1aa24caa9f0546d8623da84d087139ee931f06
|
||||
tag: ce69adfdb2902e7005d71b05a0f41143d5632ec9
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -681,14 +681,14 @@ export namespace APISetUserAutoAcceptMemberContacts {
|
||||
}
|
||||
}
|
||||
|
||||
// Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Synchronously returns `CRCmdOk`; the verification outcome is delivered asynchronously via [CEvtSimplexNameVerified](./EVENTS.md#cevtsimplexnameverified) or [CEvtSimplexNameVerifyFailed](./EVENTS.md#cevtsimplexnameverifyfailed).
|
||||
// Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Returns `CRSimplexNameVerified` with a boolean `verified` (a match also writes the verification timestamp); resolver / agent failures are reported as `CRChatCmdError`.
|
||||
// Network usage: interactive.
|
||||
export interface APIVerifySimplexName {
|
||||
chatRef: T.ChatRef
|
||||
}
|
||||
|
||||
export namespace APIVerifySimplexName {
|
||||
export type Response = CR.CmdOk | CR.ChatCmdError
|
||||
export type Response = CR.SimplexNameVerified | CR.ChatCmdError
|
||||
|
||||
export function cmdString(self: APIVerifySimplexName): string {
|
||||
return '/_verify simplex name ' + T.ChatRef.cmdString(self.chatRef)
|
||||
|
||||
@@ -9,10 +9,6 @@ export type ChatEvent =
|
||||
| CEvt.ContactDeletedByContact
|
||||
| CEvt.ReceivedContactRequest
|
||||
| CEvt.NewMemberContactReceivedInv
|
||||
| CEvt.SimplexNameConflict
|
||||
| CEvt.SimplexNameVerified
|
||||
| CEvt.SimplexNameVerifyFailed
|
||||
| CEvt.SimplexNameUnverified
|
||||
| CEvt.ContactSndReady
|
||||
| CEvt.NewChatItems
|
||||
| CEvt.ChatItemReaction
|
||||
@@ -66,10 +62,6 @@ export namespace CEvt {
|
||||
| "contactDeletedByContact"
|
||||
| "receivedContactRequest"
|
||||
| "newMemberContactReceivedInv"
|
||||
| "simplexNameConflict"
|
||||
| "simplexNameVerified"
|
||||
| "simplexNameVerifyFailed"
|
||||
| "simplexNameUnverified"
|
||||
| "contactSndReady"
|
||||
| "newChatItems"
|
||||
| "chatItemReaction"
|
||||
@@ -155,38 +147,6 @@ export namespace CEvt {
|
||||
member: T.GroupMember
|
||||
}
|
||||
|
||||
export interface SimplexNameConflict extends Interface {
|
||||
type: "simplexNameConflict"
|
||||
user: T.User
|
||||
simplexName: T.SimplexNameInfo
|
||||
entity: T.SimplexNameConflictEntity
|
||||
claimedBy: string
|
||||
displacedFrom: string
|
||||
}
|
||||
|
||||
export interface SimplexNameVerified extends Interface {
|
||||
type: "simplexNameVerified"
|
||||
user: T.User
|
||||
chatRef: T.ChatRef
|
||||
simplexName: T.SimplexNameInfo
|
||||
verifiedAt: string // ISO-8601 timestamp
|
||||
}
|
||||
|
||||
export interface SimplexNameVerifyFailed extends Interface {
|
||||
type: "simplexNameVerifyFailed"
|
||||
user: T.User
|
||||
chatRef: T.ChatRef
|
||||
simplexName: T.SimplexNameInfo
|
||||
reason: T.SimplexNameVerifyFailReason
|
||||
}
|
||||
|
||||
export interface SimplexNameUnverified extends Interface {
|
||||
type: "simplexNameUnverified"
|
||||
user: T.User
|
||||
chatRef: T.ChatRef
|
||||
simplexName: T.SimplexNameInfo
|
||||
}
|
||||
|
||||
export interface ContactSndReady extends Interface {
|
||||
type: "contactSndReady"
|
||||
user: T.User
|
||||
|
||||
@@ -48,6 +48,7 @@ export type ChatResponse =
|
||||
| CR.SentConfirmation
|
||||
| CR.SentGroupInvitation
|
||||
| CR.SentInvitation
|
||||
| CR.SimplexNameVerified
|
||||
| CR.SndFileCancelled
|
||||
| CR.UserAcceptedGroupSent
|
||||
| CR.UserContactLink
|
||||
@@ -106,6 +107,7 @@ export namespace CR {
|
||||
| "sentConfirmation"
|
||||
| "sentGroupInvitation"
|
||||
| "sentInvitation"
|
||||
| "simplexNameVerified"
|
||||
| "sndFileCancelled"
|
||||
| "userAcceptedGroupSent"
|
||||
| "userContactLink"
|
||||
@@ -407,6 +409,14 @@ export namespace CR {
|
||||
customUserProfile?: T.Profile
|
||||
}
|
||||
|
||||
export interface SimplexNameVerified extends Interface {
|
||||
type: "simplexNameVerified"
|
||||
user: T.User
|
||||
chatRef: T.ChatRef
|
||||
simplexName: T.SimplexNameInfo
|
||||
verified: boolean
|
||||
}
|
||||
|
||||
export interface SndFileCancelled extends Interface {
|
||||
type: "sndFileCancelled"
|
||||
user: T.User
|
||||
|
||||
@@ -66,6 +66,7 @@ export type AgentErrorType =
|
||||
| AgentErrorType.NTF
|
||||
| AgentErrorType.XFTP
|
||||
| AgentErrorType.FILE
|
||||
| AgentErrorType.NAME
|
||||
| AgentErrorType.PROXY
|
||||
| AgentErrorType.RCP
|
||||
| AgentErrorType.BROKER
|
||||
@@ -84,6 +85,7 @@ export namespace AgentErrorType {
|
||||
| "NTF"
|
||||
| "XFTP"
|
||||
| "FILE"
|
||||
| "NAME"
|
||||
| "PROXY"
|
||||
| "RCP"
|
||||
| "BROKER"
|
||||
@@ -136,6 +138,11 @@ export namespace AgentErrorType {
|
||||
fileErr: FileErrorType
|
||||
}
|
||||
|
||||
export interface NAME extends Interface {
|
||||
type: "NAME"
|
||||
nameErr: NameErrorType
|
||||
}
|
||||
|
||||
export interface PROXY extends Interface {
|
||||
type: "PROXY"
|
||||
proxyServer: string
|
||||
@@ -1011,7 +1018,6 @@ export type ChatErrorType =
|
||||
| ChatErrorType.InvalidConnReq
|
||||
| ChatErrorType.SimplexNameNotFound
|
||||
| ChatErrorType.SimplexNameUnprepared
|
||||
| ChatErrorType.SimplexNameResolverUnavailable
|
||||
| ChatErrorType.UnsupportedConnReq
|
||||
| ChatErrorType.ConnReqMessageProhibited
|
||||
| ChatErrorType.ContactNotReady
|
||||
@@ -1091,7 +1097,6 @@ export namespace ChatErrorType {
|
||||
| "invalidConnReq"
|
||||
| "simplexNameNotFound"
|
||||
| "simplexNameUnprepared"
|
||||
| "simplexNameResolverUnavailable"
|
||||
| "unsupportedConnReq"
|
||||
| "connReqMessageProhibited"
|
||||
| "contactNotReady"
|
||||
@@ -1256,11 +1261,6 @@ export namespace ChatErrorType {
|
||||
simplexName: SimplexNameInfo
|
||||
}
|
||||
|
||||
export interface SimplexNameResolverUnavailable extends Interface {
|
||||
type: "simplexNameResolverUnavailable"
|
||||
simplexName: SimplexNameInfo
|
||||
}
|
||||
|
||||
export interface UnsupportedConnReq extends Interface {
|
||||
type: "unsupportedConnReq"
|
||||
}
|
||||
@@ -2174,6 +2174,7 @@ export type ErrorType =
|
||||
| ErrorType.LARGE_MSG
|
||||
| ErrorType.EXPIRED
|
||||
| ErrorType.INTERNAL
|
||||
| ErrorType.NAME
|
||||
| ErrorType.DUPLICATE_
|
||||
|
||||
export namespace ErrorType {
|
||||
@@ -2192,6 +2193,7 @@ export namespace ErrorType {
|
||||
| "LARGE_MSG"
|
||||
| "EXPIRED"
|
||||
| "INTERNAL"
|
||||
| "NAME"
|
||||
| "DUPLICATE_"
|
||||
|
||||
interface Interface {
|
||||
@@ -2258,6 +2260,11 @@ export namespace ErrorType {
|
||||
type: "INTERNAL"
|
||||
}
|
||||
|
||||
export interface NAME extends Interface {
|
||||
type: "NAME"
|
||||
nameErr: NameErrorType
|
||||
}
|
||||
|
||||
export interface DUPLICATE_ extends Interface {
|
||||
type: "DUPLICATE_"
|
||||
}
|
||||
@@ -3186,6 +3193,37 @@ export enum MsgSigStatus {
|
||||
SignedNoKey = "signedNoKey",
|
||||
}
|
||||
|
||||
export type NameErrorType =
|
||||
| NameErrorType.NO_RESOLVER
|
||||
| NameErrorType.NO_NAME
|
||||
| NameErrorType.NO_SERVERS
|
||||
| NameErrorType.RESOLVER
|
||||
|
||||
export namespace NameErrorType {
|
||||
export type Tag = "NO_RESOLVER" | "NO_NAME" | "NO_SERVERS" | "RESOLVER"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface NO_RESOLVER extends Interface {
|
||||
type: "NO_RESOLVER"
|
||||
}
|
||||
|
||||
export interface NO_NAME extends Interface {
|
||||
type: "NO_NAME"
|
||||
}
|
||||
|
||||
export interface NO_SERVERS extends Interface {
|
||||
type: "NO_SERVERS"
|
||||
}
|
||||
|
||||
export interface RESOLVER extends Interface {
|
||||
type: "RESOLVER"
|
||||
resolverErr: string
|
||||
}
|
||||
}
|
||||
|
||||
export type NetworkError =
|
||||
| NetworkError.ConnectError
|
||||
| NetworkError.TLSError
|
||||
@@ -3913,11 +3951,6 @@ export enum SimplexLinkType {
|
||||
Relay = "relay",
|
||||
}
|
||||
|
||||
export enum SimplexNameConflictEntity {
|
||||
Contact = "contact",
|
||||
Group = "group",
|
||||
}
|
||||
|
||||
export interface SimplexNameDomain {
|
||||
nameTLD: SimplexTLD
|
||||
domain: string
|
||||
@@ -3934,32 +3967,6 @@ export enum SimplexNameType {
|
||||
Contact = "contact",
|
||||
}
|
||||
|
||||
export type SimplexNameVerifyFailReason =
|
||||
| SimplexNameVerifyFailReason.LinkMismatch
|
||||
| SimplexNameVerifyFailReason.NameNotRegistered
|
||||
| SimplexNameVerifyFailReason.ResolverError
|
||||
|
||||
export namespace SimplexNameVerifyFailReason {
|
||||
export type Tag = "linkMismatch" | "nameNotRegistered" | "resolverError"
|
||||
|
||||
interface Interface {
|
||||
type: Tag
|
||||
}
|
||||
|
||||
export interface LinkMismatch extends Interface {
|
||||
type: "linkMismatch"
|
||||
}
|
||||
|
||||
export interface NameNotRegistered extends Interface {
|
||||
type: "nameNotRegistered"
|
||||
}
|
||||
|
||||
export interface ResolverError extends Interface {
|
||||
type: "resolverError"
|
||||
agentError: AgentErrorType
|
||||
}
|
||||
}
|
||||
|
||||
export enum SimplexTLD {
|
||||
Simplex = "simplex",
|
||||
Testing = "testing",
|
||||
|
||||
@@ -596,7 +596,7 @@ def APISetUserAutoAcceptMemberContacts_cmd_string(self: APISetUserAutoAcceptMemb
|
||||
APISetUserAutoAcceptMemberContacts_Response = CR.CmdOk | CR.ChatCmdError
|
||||
|
||||
|
||||
# Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Synchronously returns `CRCmdOk`; the verification outcome is delivered asynchronously via [CEvtSimplexNameVerified](./EVENTS.md#cevtsimplexnameverified) or [CEvtSimplexNameVerifyFailed](./EVENTS.md#cevtsimplexnameverifyfailed).
|
||||
# Verify a contact's or group's claimed SimpleX name by RSLV-resolving the claim and comparing the resolved link to the peer's stored connection link. Returns `CRSimplexNameVerified` with a boolean `verified` (a match also writes the verification timestamp); resolver / agent failures are reported as `CRChatCmdError`.
|
||||
# Network usage: interactive.
|
||||
class APIVerifySimplexName(TypedDict):
|
||||
chatRef: "T.ChatRef"
|
||||
@@ -605,7 +605,7 @@ class APIVerifySimplexName(TypedDict):
|
||||
def APIVerifySimplexName_cmd_string(self: APIVerifySimplexName) -> str:
|
||||
return '/_verify simplex name ' + T.ChatRef_cmd_string(self['chatRef'])
|
||||
|
||||
APIVerifySimplexName_Response = CR.CmdOk | CR.ChatCmdError
|
||||
APIVerifySimplexName_Response = CR.SimplexNameVerified | CR.ChatCmdError
|
||||
|
||||
|
||||
# User profile commands
|
||||
|
||||
@@ -35,34 +35,6 @@ class NewMemberContactReceivedInv(TypedDict):
|
||||
groupInfo: "T.GroupInfo"
|
||||
member: "T.GroupMember"
|
||||
|
||||
class SimplexNameConflict(TypedDict):
|
||||
type: Literal["simplexNameConflict"]
|
||||
user: "T.User"
|
||||
simplexName: "T.SimplexNameInfo"
|
||||
entity: "T.SimplexNameConflictEntity"
|
||||
claimedBy: str
|
||||
displacedFrom: str
|
||||
|
||||
class SimplexNameVerified(TypedDict):
|
||||
type: Literal["simplexNameVerified"]
|
||||
user: "T.User"
|
||||
chatRef: "T.ChatRef"
|
||||
simplexName: "T.SimplexNameInfo"
|
||||
verifiedAt: str # ISO-8601 timestamp
|
||||
|
||||
class SimplexNameVerifyFailed(TypedDict):
|
||||
type: Literal["simplexNameVerifyFailed"]
|
||||
user: "T.User"
|
||||
chatRef: "T.ChatRef"
|
||||
simplexName: "T.SimplexNameInfo"
|
||||
reason: "T.SimplexNameVerifyFailReason"
|
||||
|
||||
class SimplexNameUnverified(TypedDict):
|
||||
type: Literal["simplexNameUnverified"]
|
||||
user: "T.User"
|
||||
chatRef: "T.ChatRef"
|
||||
simplexName: "T.SimplexNameInfo"
|
||||
|
||||
class ContactSndReady(TypedDict):
|
||||
type: Literal["contactSndReady"]
|
||||
user: "T.User"
|
||||
@@ -358,10 +330,6 @@ ChatEvent = (
|
||||
| ContactDeletedByContact
|
||||
| ReceivedContactRequest
|
||||
| NewMemberContactReceivedInv
|
||||
| SimplexNameConflict
|
||||
| SimplexNameVerified
|
||||
| SimplexNameVerifyFailed
|
||||
| SimplexNameUnverified
|
||||
| ContactSndReady
|
||||
| NewChatItems
|
||||
| ChatItemReaction
|
||||
@@ -409,7 +377,7 @@ ChatEvent = (
|
||||
| ChatErrors
|
||||
)
|
||||
|
||||
ChatEvent_Tag = Literal["contactConnected", "contactUpdated", "contactDeletedByContact", "receivedContactRequest", "newMemberContactReceivedInv", "simplexNameConflict", "simplexNameVerified", "simplexNameVerifyFailed", "simplexNameUnverified", "contactSndReady", "newChatItems", "chatItemReaction", "chatItemsDeleted", "chatItemUpdated", "groupChatItemsDeleted", "chatItemsStatusesUpdated", "receivedGroupInvitation", "userJoinedGroup", "groupUpdated", "joinedGroupMember", "memberRole", "deletedMember", "leftMember", "deletedMemberUser", "groupDeleted", "connectedToGroupMember", "memberAcceptedByOther", "memberBlockedForAll", "groupMemberUpdated", "groupLinkDataUpdated", "groupRelayUpdated", "rcvFileDescrReady", "rcvFileComplete", "sndFileCompleteXFTP", "rcvFileStart", "rcvFileSndCancelled", "rcvFileAccepted", "rcvFileError", "rcvFileWarning", "sndFileError", "sndFileWarning", "acceptingContactRequest", "acceptingBusinessRequest", "contactConnecting", "businessLinkConnecting", "joinedGroupMemberConnecting", "sentGroupInvitation", "groupLinkConnecting", "hostConnected", "hostDisconnected", "subscriptionStatus", "messageError", "chatError", "chatErrors"]
|
||||
ChatEvent_Tag = Literal["contactConnected", "contactUpdated", "contactDeletedByContact", "receivedContactRequest", "newMemberContactReceivedInv", "contactSndReady", "newChatItems", "chatItemReaction", "chatItemsDeleted", "chatItemUpdated", "groupChatItemsDeleted", "chatItemsStatusesUpdated", "receivedGroupInvitation", "userJoinedGroup", "groupUpdated", "joinedGroupMember", "memberRole", "deletedMember", "leftMember", "deletedMemberUser", "groupDeleted", "connectedToGroupMember", "memberAcceptedByOther", "memberBlockedForAll", "groupMemberUpdated", "groupLinkDataUpdated", "groupRelayUpdated", "rcvFileDescrReady", "rcvFileComplete", "sndFileCompleteXFTP", "rcvFileStart", "rcvFileSndCancelled", "rcvFileAccepted", "rcvFileError", "rcvFileWarning", "sndFileError", "sndFileWarning", "acceptingContactRequest", "acceptingBusinessRequest", "contactConnecting", "businessLinkConnecting", "joinedGroupMemberConnecting", "sentGroupInvitation", "groupLinkConnecting", "hostConnected", "hostDisconnected", "subscriptionStatus", "messageError", "chatError", "chatErrors"]
|
||||
|
||||
|
||||
class OnEventDecorator(Protocol):
|
||||
@@ -450,30 +418,6 @@ class OnEventDecorator(Protocol):
|
||||
Callable[["NewMemberContactReceivedInv"], Awaitable[None]],
|
||||
]: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, event: Literal["simplexNameConflict"], /) -> Callable[
|
||||
[Callable[["SimplexNameConflict"], Awaitable[None]]],
|
||||
Callable[["SimplexNameConflict"], Awaitable[None]],
|
||||
]: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, event: Literal["simplexNameVerified"], /) -> Callable[
|
||||
[Callable[["SimplexNameVerified"], Awaitable[None]]],
|
||||
Callable[["SimplexNameVerified"], Awaitable[None]],
|
||||
]: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, event: Literal["simplexNameVerifyFailed"], /) -> Callable[
|
||||
[Callable[["SimplexNameVerifyFailed"], Awaitable[None]]],
|
||||
Callable[["SimplexNameVerifyFailed"], Awaitable[None]],
|
||||
]: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, event: Literal["simplexNameUnverified"], /) -> Callable[
|
||||
[Callable[["SimplexNameUnverified"], Awaitable[None]]],
|
||||
Callable[["SimplexNameUnverified"], Awaitable[None]],
|
||||
]: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, event: Literal["contactSndReady"], /) -> Callable[
|
||||
[Callable[["ContactSndReady"], Awaitable[None]]],
|
||||
|
||||
@@ -245,6 +245,13 @@ class SentInvitation(TypedDict):
|
||||
connection: "T.PendingContactConnection"
|
||||
customUserProfile: NotRequired["T.Profile"]
|
||||
|
||||
class SimplexNameVerified(TypedDict):
|
||||
type: Literal["simplexNameVerified"]
|
||||
user: "T.User"
|
||||
chatRef: "T.ChatRef"
|
||||
simplexName: "T.SimplexNameInfo"
|
||||
verified: bool
|
||||
|
||||
class SndFileCancelled(TypedDict):
|
||||
type: Literal["sndFileCancelled"]
|
||||
user: "T.User"
|
||||
@@ -350,6 +357,7 @@ ChatResponse = (
|
||||
| SentConfirmation
|
||||
| SentGroupInvitation
|
||||
| SentInvitation
|
||||
| SimplexNameVerified
|
||||
| SndFileCancelled
|
||||
| UserAcceptedGroupSent
|
||||
| UserContactLink
|
||||
@@ -363,4 +371,4 @@ ChatResponse = (
|
||||
| ApiChats
|
||||
)
|
||||
|
||||
ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "relayGroupAllowed", "groupMembers", "groupUpdated", "groupsList", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "sentConfirmation", "sentGroupInvitation", "sentInvitation", "sndFileCancelled", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"]
|
||||
ChatResponse_Tag = Literal["acceptingContactRequest", "activeUser", "chatItemNotChanged", "chatItemReaction", "chatItemUpdated", "chatItemsDeleted", "chatRunning", "chatStarted", "chatStopped", "cmdOk", "chatCmdError", "connectionPlan", "contactAlreadyExists", "contactConnectionDeleted", "contactDeleted", "contactPrefsUpdated", "contactRequestRejected", "contactsList", "groupDeletedUser", "groupLink", "groupLinkCreated", "groupLinkDeleted", "groupCreated", "publicGroupCreated", "publicGroupCreationFailed", "groupRelays", "groupRelaysAdded", "groupRelaysAddFailed", "relayGroupAllowed", "groupMembers", "groupUpdated", "groupsList", "invitation", "leftMemberUser", "memberAccepted", "membersBlockedForAllUser", "membersRoleUser", "newChatItems", "rcvFileAccepted", "rcvFileAcceptedSndCancelled", "rcvFileCancelled", "sentConfirmation", "sentGroupInvitation", "sentInvitation", "simplexNameVerified", "sndFileCancelled", "userAcceptedGroupSent", "userContactLink", "userContactLinkCreated", "userContactLinkDeleted", "userContactLinkUpdated", "userDeletedMembers", "userProfileUpdated", "userProfileNoChange", "usersList", "apiChats"]
|
||||
|
||||
@@ -78,6 +78,10 @@ class AgentErrorType_FILE(TypedDict):
|
||||
type: Literal["FILE"]
|
||||
fileErr: "FileErrorType"
|
||||
|
||||
class AgentErrorType_NAME(TypedDict):
|
||||
type: Literal["NAME"]
|
||||
nameErr: "NameErrorType"
|
||||
|
||||
class AgentErrorType_PROXY(TypedDict):
|
||||
type: Literal["PROXY"]
|
||||
proxyServer: str
|
||||
@@ -123,6 +127,7 @@ AgentErrorType = (
|
||||
| AgentErrorType_NTF
|
||||
| AgentErrorType_XFTP
|
||||
| AgentErrorType_FILE
|
||||
| AgentErrorType_NAME
|
||||
| AgentErrorType_PROXY
|
||||
| AgentErrorType_RCP
|
||||
| AgentErrorType_BROKER
|
||||
@@ -133,7 +138,7 @@ AgentErrorType = (
|
||||
| AgentErrorType_INACTIVE
|
||||
)
|
||||
|
||||
AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"]
|
||||
AgentErrorType_Tag = Literal["CMD", "CONN", "NO_USER", "SMP", "NTF", "XFTP", "FILE", "NAME", "PROXY", "RCP", "BROKER", "AGENT", "NOTICE", "INTERNAL", "CRITICAL", "INACTIVE"]
|
||||
|
||||
class AutoAccept(TypedDict):
|
||||
acceptIncognito: bool
|
||||
@@ -777,10 +782,6 @@ class ChatErrorType_simplexNameUnprepared(TypedDict):
|
||||
type: Literal["simplexNameUnprepared"]
|
||||
simplexName: "SimplexNameInfo"
|
||||
|
||||
class ChatErrorType_simplexNameResolverUnavailable(TypedDict):
|
||||
type: Literal["simplexNameResolverUnavailable"]
|
||||
simplexName: "SimplexNameInfo"
|
||||
|
||||
class ChatErrorType_unsupportedConnReq(TypedDict):
|
||||
type: Literal["unsupportedConnReq"]
|
||||
|
||||
@@ -1013,7 +1014,6 @@ ChatErrorType = (
|
||||
| ChatErrorType_invalidConnReq
|
||||
| ChatErrorType_simplexNameNotFound
|
||||
| ChatErrorType_simplexNameUnprepared
|
||||
| ChatErrorType_simplexNameResolverUnavailable
|
||||
| ChatErrorType_unsupportedConnReq
|
||||
| ChatErrorType_connReqMessageProhibited
|
||||
| ChatErrorType_contactNotReady
|
||||
@@ -1070,7 +1070,7 @@ ChatErrorType = (
|
||||
| ChatErrorType_exception
|
||||
)
|
||||
|
||||
ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "simplexNameNotFound", "simplexNameUnprepared", "simplexNameResolverUnavailable", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"]
|
||||
ChatErrorType_Tag = Literal["noActiveUser", "noConnectionUser", "noSndFileUser", "noRcvFileUser", "userUnknown", "userExists", "chatRelayExists", "differentActiveUser", "cantDeleteActiveUser", "cantDeleteLastUser", "cantHideLastUser", "hiddenUserAlwaysMuted", "emptyUserPassword", "userAlreadyHidden", "userNotHidden", "invalidDisplayName", "chatNotStarted", "chatNotStopped", "chatStoreChanged", "invalidConnReq", "simplexNameNotFound", "simplexNameUnprepared", "unsupportedConnReq", "connReqMessageProhibited", "contactNotReady", "contactNotActive", "contactDisabled", "connectionDisabled", "groupUserRole", "groupMemberInitialRole", "contactIncognitoCantInvite", "groupIncognitoCantInvite", "groupContactRole", "groupDuplicateMember", "groupDuplicateMemberId", "groupNotJoined", "groupMemberNotActive", "cantBlockMemberForSelf", "groupMemberUserRemoved", "groupMemberNotFound", "groupCantResendInvitation", "groupInternal", "fileNotFound", "fileSize", "fileAlreadyReceiving", "fileCancelled", "fileCancel", "fileAlreadyExists", "fileWrite", "fileSend", "fileRcvChunk", "fileInternal", "fileImageType", "fileImageSize", "fileNotReceived", "fileNotApproved", "fallbackToSMPProhibited", "inlineFileProhibited", "invalidForward", "invalidChatItemUpdate", "invalidChatItemDelete", "hasCurrentCall", "noCurrentCall", "callContact", "directMessagesProhibited", "agentVersion", "agentNoSubResult", "commandError", "agentCommandError", "invalidFileDescription", "connectionIncognitoChangeProhibited", "connectionUserChangeProhibited", "peerChatVRangeIncompatible", "relayTestError", "internalError", "exception"]
|
||||
|
||||
ChatFeature = Literal["timedMessages", "fullDelete", "reactions", "voice", "files", "calls", "sessions"]
|
||||
|
||||
@@ -1569,6 +1569,10 @@ class ErrorType_EXPIRED(TypedDict):
|
||||
class ErrorType_INTERNAL(TypedDict):
|
||||
type: Literal["INTERNAL"]
|
||||
|
||||
class ErrorType_NAME(TypedDict):
|
||||
type: Literal["NAME"]
|
||||
nameErr: "NameErrorType"
|
||||
|
||||
class ErrorType_DUPLICATE_(TypedDict):
|
||||
type: Literal["DUPLICATE_"]
|
||||
|
||||
@@ -1587,10 +1591,11 @@ ErrorType = (
|
||||
| ErrorType_LARGE_MSG
|
||||
| ErrorType_EXPIRED
|
||||
| ErrorType_INTERNAL
|
||||
| ErrorType_NAME
|
||||
| ErrorType_DUPLICATE_
|
||||
)
|
||||
|
||||
ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "DUPLICATE_"]
|
||||
ErrorType_Tag = Literal["BLOCK", "SESSION", "CMD", "PROXY", "AUTH", "BLOCKED", "SERVICE", "CRYPTO", "QUOTA", "STORE", "NO_MSG", "LARGE_MSG", "EXPIRED", "INTERNAL", "NAME", "DUPLICATE_"]
|
||||
|
||||
FeatureAllowed = Literal["always", "yes", "no"]
|
||||
|
||||
@@ -2233,6 +2238,28 @@ MsgReceiptStatus = Literal["ok", "badMsgHash"]
|
||||
|
||||
MsgSigStatus = Literal["verified", "signedNoKey"]
|
||||
|
||||
class NameErrorType_NO_RESOLVER(TypedDict):
|
||||
type: Literal["NO_RESOLVER"]
|
||||
|
||||
class NameErrorType_NO_NAME(TypedDict):
|
||||
type: Literal["NO_NAME"]
|
||||
|
||||
class NameErrorType_NO_SERVERS(TypedDict):
|
||||
type: Literal["NO_SERVERS"]
|
||||
|
||||
class NameErrorType_RESOLVER(TypedDict):
|
||||
type: Literal["RESOLVER"]
|
||||
resolverErr: str
|
||||
|
||||
NameErrorType = (
|
||||
NameErrorType_NO_RESOLVER
|
||||
| NameErrorType_NO_NAME
|
||||
| NameErrorType_NO_SERVERS
|
||||
| NameErrorType_RESOLVER
|
||||
)
|
||||
|
||||
NameErrorType_Tag = Literal["NO_RESOLVER", "NO_NAME", "NO_SERVERS", "RESOLVER"]
|
||||
|
||||
class NetworkError_connectError(TypedDict):
|
||||
type: Literal["connectError"]
|
||||
connectError: str
|
||||
@@ -2737,8 +2764,6 @@ class SimplePreference(TypedDict):
|
||||
|
||||
SimplexLinkType = Literal["contact", "invitation", "group", "channel", "relay"]
|
||||
|
||||
SimplexNameConflictEntity = Literal["contact", "group"]
|
||||
|
||||
class SimplexNameDomain(TypedDict):
|
||||
nameTLD: "SimplexTLD"
|
||||
domain: str
|
||||
@@ -2750,24 +2775,6 @@ class SimplexNameInfo(TypedDict):
|
||||
|
||||
SimplexNameType = Literal["publicGroup", "contact"]
|
||||
|
||||
class SimplexNameVerifyFailReason_linkMismatch(TypedDict):
|
||||
type: Literal["linkMismatch"]
|
||||
|
||||
class SimplexNameVerifyFailReason_nameNotRegistered(TypedDict):
|
||||
type: Literal["nameNotRegistered"]
|
||||
|
||||
class SimplexNameVerifyFailReason_resolverError(TypedDict):
|
||||
type: Literal["resolverError"]
|
||||
agentError: "AgentErrorType"
|
||||
|
||||
SimplexNameVerifyFailReason = (
|
||||
SimplexNameVerifyFailReason_linkMismatch
|
||||
| SimplexNameVerifyFailReason_nameNotRegistered
|
||||
| SimplexNameVerifyFailReason_resolverError
|
||||
)
|
||||
|
||||
SimplexNameVerifyFailReason_Tag = Literal["linkMismatch", "nameNotRegistered", "resolverError"]
|
||||
|
||||
SimplexTLD = Literal["simplex", "testing", "web"]
|
||||
|
||||
SndCIStatusProgress = Literal["partial", "complete"]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."df1aa24caa9f0546d8623da84d087139ee931f06" = "1can7rfjwh2nprwg1vw4719n9n72wmbd63j98zwmqxhi5xfydfjc";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."ce69adfdb2902e7005d71b05a0f41143d5632ec9" = "0jmcxk0q8fa91a86sqq7xh9dwi8jvccq1nwkgm00av6la78z1izy";
|
||||
"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";
|
||||
|
||||
@@ -140,6 +140,7 @@ library
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260604_simplex_name_profiles
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260606_simplex_name_verified
|
||||
Simplex.Chat.Store.Postgres.Migrations.M20260612_smp_role_names
|
||||
else
|
||||
exposed-modules:
|
||||
Simplex.Chat.Archive
|
||||
@@ -302,6 +303,7 @@ library
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260604_simplex_name_profiles
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260606_simplex_name_verified
|
||||
Simplex.Chat.Store.SQLite.Migrations.M20260612_smp_role_names
|
||||
other-modules:
|
||||
Paths_simplex_chat
|
||||
hs-source-dirs:
|
||||
|
||||
@@ -494,8 +494,8 @@ data ChatCommand
|
||||
| Connect {incognito :: IncognitoEnabled, connTarget_ :: Maybe ConnectTarget}
|
||||
| -- Resolves the simplex_name claim on the chat row (contact or group) via
|
||||
-- RSLV and compares the resolved link to the peer's stored connection link.
|
||||
-- On match: writes simplex_name_verified_at and emits CEvtSimplexNameVerified.
|
||||
-- On link mismatch or resolver failure: emits CEvtSimplexNameVerifyFailed.
|
||||
-- Returns CRSimplexNameVerified with the boolean result (a match writes
|
||||
-- simplex_name_verified_at); resolver / agent failures surface as ChatErrorAgent.
|
||||
APIVerifySimplexName {chatRef :: ChatRef}
|
||||
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
|
||||
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
|
||||
@@ -733,6 +733,7 @@ data ChatResponse
|
||||
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
|
||||
| CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
|
||||
| CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text}
|
||||
| CRSimplexNameVerified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, verified :: Bool}
|
||||
| CRTagsUpdated {user :: User, userTags :: [ChatTag], chatTags :: [ChatTagId]}
|
||||
| CRNewChatItems {user :: User, chatItems :: [AChatItem]}
|
||||
| CRChatItemUpdated {user :: User, chatItem :: AChatItem}
|
||||
@@ -957,46 +958,8 @@ data ChatEvent
|
||||
| CEvtTimedAction {action :: String, durationMilliseconds :: Int64}
|
||||
| CEvtTerminalEvent TerminalEvent
|
||||
| CEvtCustomChatEvent {user_ :: Maybe User, response :: Text}
|
||||
| -- Emitted when an incoming peer Profile / GroupProfile carrying a
|
||||
-- simplexName collides with another row in the same user's DB that
|
||||
-- already holds that name. The older row's simplex_name is NULLed
|
||||
-- (newer-claim-wins per RSLV); displacedFrom is the old row's local
|
||||
-- display_name, claimedBy is the peer / group whose claim won.
|
||||
CEvtSimplexNameConflict {user :: User, simplexName :: SimplexNameInfo, entity :: SimplexNameConflictEntity, claimedBy :: ContactName, displacedFrom :: ContactName}
|
||||
| -- Emitted by APIVerifySimplexName when the RSLV-resolved link for the
|
||||
-- claimed name matches the peer's stored connection link. simplex_name_verified_at
|
||||
-- has been written; UI should clear the unverified indicator.
|
||||
CEvtSimplexNameVerified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, verifiedAt :: UTCTime}
|
||||
| -- Emitted by APIVerifySimplexName when verification did not succeed.
|
||||
-- The simplex_name claim is NOT cleared; the user may still wish to keep
|
||||
-- the contact/group. UI should surface the failure reason.
|
||||
CEvtSimplexNameVerifyFailed {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, reason :: SimplexNameVerifyFailReason}
|
||||
| -- Passive warning emitted when an incoming XInfo / XGrpInfo carries a
|
||||
-- simplex_name claim that the user has not (yet) verified — i.e.
|
||||
-- simplex_name_verified_at is NULL. The UI is expected to show an
|
||||
-- unverified indicator; the user can invoke APIVerifySimplexName to clear it.
|
||||
CEvtSimplexNameUnverified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo}
|
||||
deriving (Show)
|
||||
|
||||
data SimplexNameConflictEntity = SNCEContact | SNCEGroup
|
||||
deriving (Show)
|
||||
|
||||
-- | Why APIVerifySimplexName failed. The resolved record is not stashed: we
|
||||
-- intentionally do not allow a "verified to point at a DIFFERENT contact"
|
||||
-- state; the user must decide whether to keep the existing contact or start
|
||||
-- a fresh connection with the resolved link.
|
||||
data SimplexNameVerifyFailReason
|
||||
= -- | Resolver returned a NameRecord but its link for this entity's type
|
||||
-- (nrSimplexContact for contacts, nrSimplexChannel for groups) differs
|
||||
-- from the link stored locally for the peer.
|
||||
SNVFLinkMismatch
|
||||
| -- | RSLV returned AUTH (NameNotRegistered): no on-chain record exists.
|
||||
SNVFNameNotRegistered
|
||||
| -- | Transport / proxy / other resolver-side failure. The agent error is
|
||||
-- surfaced verbatim so the UI can reuse existing agent-error rendering.
|
||||
SNVFResolverError {agentError :: AgentErrorType}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data TerminalEvent
|
||||
= TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason}
|
||||
| TERelayRejected {user :: User, groupInfo :: GroupInfo, relayRejectionReason :: RelayRejectionReason}
|
||||
@@ -1419,7 +1382,6 @@ data ChatErrorType
|
||||
| CEInvalidConnReq
|
||||
| CESimplexNameNotFound {simplexName :: SimplexNameInfo}
|
||||
| CESimplexNameUnprepared {simplexName :: SimplexNameInfo}
|
||||
| CESimplexNameResolverUnavailable {simplexName :: SimplexNameInfo}
|
||||
| CEUnsupportedConnReq
|
||||
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
|
||||
| CEConnReqMessageProhibited
|
||||
@@ -1821,10 +1783,6 @@ $(JQ.deriveJSON defaultJSON ''RelayTestFailure)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
|
||||
|
||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "SNCE") ''SimplexNameConflictEntity)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SNVF") ''SimplexNameVerifyFailReason)
|
||||
|
||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CEvt") ''ChatEvent)
|
||||
|
||||
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
|
||||
|
||||
@@ -98,7 +98,7 @@ 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 (..), ProxyClientError (..), SMPWebPortServers (..), SocksMode (SMAlways), pattern NRMInteractive, textToHostMode)
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import qualified Simplex.Messaging.Crypto.ShortLink as SL
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
@@ -107,8 +107,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), patt
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (base64P)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SMPServer, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
|
||||
@@ -2034,10 +2033,7 @@ processChatCommand cxt nm = \case
|
||||
_ -> Chat cInfo [] emptyChatStats
|
||||
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
|
||||
ACCL _ (CCLink cReq _) -> do
|
||||
(ct, displaced_) <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId Nothing
|
||||
let Profile {simplexName = pSimplexName} = profile
|
||||
Contact {localDisplayName = newLDN} = ct
|
||||
surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN
|
||||
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId Nothing
|
||||
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart)
|
||||
let cd = CDDirectRcv ct
|
||||
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing
|
||||
@@ -2239,16 +2235,15 @@ processChatCommand cxt nm = \case
|
||||
CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct
|
||||
CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile
|
||||
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
|
||||
Connect incognito (Just (CTLink cLink@(ACL m cLink'))) -> withUser $ \user -> do
|
||||
Connect incognito (Just ct) -> withUser $ \user -> do
|
||||
-- TODO [relays] member: /c api to support groups with relays
|
||||
-- TODO - possibly by going through APIPrepareGroup -> APIConnectPreparedGroup
|
||||
(ccLink, plan) <- connectPlanLink user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e
|
||||
connectWithPlan user incognito ccLink plan
|
||||
Connect incognito (Just (CTName ni)) -> withUser $ \user -> do
|
||||
(ccLink, plan) <- connectPlanName user ni
|
||||
(ccLink, plan) <- connectPlan user ct False Nothing `catchAllErrors` \e -> case ct of
|
||||
CTLink (ACL m (CLFull cReq)) -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing))
|
||||
_ -> throwError e
|
||||
connectWithPlan user incognito ccLink plan
|
||||
Connect _ Nothing -> throwChatError CEInvalidConnReq
|
||||
APIVerifySimplexName chatRef -> withUser $ \user -> apiVerifySimplexName user chatRef
|
||||
APIVerifySimplexName chatRef -> withUser $ \user -> apiVerifySimplexName user nm chatRef
|
||||
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
|
||||
ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId
|
||||
ccLink <- case contactLink of
|
||||
@@ -4203,10 +4198,12 @@ processChatCommand cxt nm = \case
|
||||
Nothing -> resolveAndDispatch
|
||||
where
|
||||
resolveAndDispatch :: CM (ACreatedConnLink, ConnectionPlan)
|
||||
resolveAndDispatch =
|
||||
resolveOnUserServers user nameDomain >>= \case
|
||||
resolveAndDispatch = do
|
||||
a <- asks smpAgent
|
||||
let User {userId} = user
|
||||
liftIO (runExceptT $ resolveSimplexName a nm userId nameDomain) >>= \case
|
||||
Right nr -> dispatchResolvedRecord cxt nm user ni nr
|
||||
Left re -> throwError $ resolveErrorToChatError ni re
|
||||
Left e -> throwError $ chatErrorAgent e
|
||||
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
|
||||
connectWithPlan user@User {userId} incognito ccLink plan
|
||||
| connectionPlanProceed plan = do
|
||||
@@ -4631,58 +4628,6 @@ processChatCommand cxt nm = \case
|
||||
gVar <- asks random
|
||||
liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12
|
||||
|
||||
-- | Failure modes for 'resolveOnUserServers' / 'iterateResolvers'.
|
||||
data ResolveError
|
||||
= -- | No enabled server can resolve: every candidate answered CMD UNKNOWN
|
||||
-- (predates RSLV) or CMD PROHIBITED (speaks RSLV but has no resolver
|
||||
-- configured), or none were configured / reachable. Iterating across these is
|
||||
-- safe: a CMD UNKNOWN relay never received the name (the client degrades RSLV
|
||||
-- to a no-op below namesSMPVersion, see Protocol.hs); a CMD PROHIBITED relay
|
||||
-- did receive it but is one the user already trusts as an SMP server.
|
||||
ResolverUnavailable
|
||||
| -- | AUTH from a name-capable server. Every name server reads the same on-chain state, so we trust the first one's no.
|
||||
NameNotRegistered
|
||||
| -- | First server returned a definite non-transport error (proxy, protocol, etc).
|
||||
-- Surface to user so they can retry; do not iterate, for the same privacy reason.
|
||||
ResolverTransport AgentErrorType
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | Return the user's enabled SMP servers (preset and custom, excluding deleted).
|
||||
-- Applies the same operator-aware filter the agent uses ('agentServerCfgs'): a
|
||||
-- server is included only if it is enabled, not deleted, and either custom (no
|
||||
-- operator) or owned by an enabled operator. Disabling an operator thus removes
|
||||
-- its servers from name resolution, matching the rest of the app.
|
||||
enabledSMPServersForUser :: User -> CM [SMPServer]
|
||||
enabledSMPServersForUser user = do
|
||||
ops <- serverOperators <$> withFastStore getServerOperators
|
||||
smpSrvs <- withFastStore' $ \db -> getProtocolServers db SPSMP user
|
||||
let opDomains = operatorDomains ops
|
||||
cfgs = agentServerCfgs SPSMP opDomains $ filter (\UserServer {deleted} -> not deleted) smpSrvs
|
||||
pure $ mapMaybe enabledSrv cfgs
|
||||
where
|
||||
enabledSrv ServerCfg {server = ProtoServerWithAuth srv _, enabled}
|
||||
| enabled = Just srv
|
||||
| otherwise = Nothing
|
||||
|
||||
-- | Resolve a SimpleX name by trying the user's enabled SMP servers in order.
|
||||
-- Transport-level failures (NETWORK, TIMEOUT, host-unreachable) and servers that
|
||||
-- cannot resolve (CMD UNKNOWN -- predates RSLV; or CMD PROHIBITED -- speaks RSLV
|
||||
-- but has no resolver configured) all fall through to the next server. A CMD
|
||||
-- UNKNOWN relay never received the name (the client degrades RSLV below
|
||||
-- namesSMPVersion); a CMD PROHIBITED relay did, but it is one the user already
|
||||
-- trusts as an SMP server. A definitive answer from a name-capable relay
|
||||
-- terminates iteration: AUTH is definitive NotFound (every name server reads the
|
||||
-- same on-chain state); any other definite error (e.g. INTERNAL on a resolver
|
||||
-- backend failure) surfaces as ResolverTransport.
|
||||
-- Privacy: a name-capable relay does see the queried name, so once one has
|
||||
-- answered we do not broadcast the miss to every other operator the user has.
|
||||
resolveOnUserServers :: User -> SimplexNameDomain -> CM (Either ResolveError NameRecord)
|
||||
resolveOnUserServers user@User {userId} domain = do
|
||||
srvs <- enabledSMPServersForUser user
|
||||
a <- asks smpAgent
|
||||
iterateResolvers srvs $ \srv ->
|
||||
liftIO . runExceptT $ resolveSimplexName a NRMInteractive userId srv domain
|
||||
|
||||
-- | Dispatch a resolved NameRecord by eagerly preparing a contact/group row
|
||||
-- with @simplex_name@ set, then returning the same plan shape ('CAPKnown' /
|
||||
-- 'GLPKnown') the local-store-hit branch of 'connectPlanName' returns. The
|
||||
@@ -4714,10 +4659,7 @@ dispatchResolvedRecord cxt nm user ni@SimplexNameInfo {nameType} NameRecord {nrS
|
||||
liftIO (decodeLinkUserData cData) >>= maybe (throwError $ chatErrorAgent $ AGENT $ A_LINK "could not decode contact profile from RSLV link") pure
|
||||
let ccLink = CCLink cReq (Just l')
|
||||
accLink = ACCL SCMContact ccLink
|
||||
(ct, displaced_) <- withStore $ \db -> createPreparedContact db cxt user profile accLink Nothing (Just ni)
|
||||
let Profile {simplexName = pSimplexName} = profile
|
||||
Contact {localDisplayName = newLDN} = ct
|
||||
surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN
|
||||
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink Nothing (Just ni)
|
||||
pure (accLink, CPContactAddress (CAPKnown ct))
|
||||
prepareGroup :: ConnShortLink 'CMContact -> CM (ACreatedConnLink, ConnectionPlan)
|
||||
prepareGroup l = do
|
||||
@@ -4756,17 +4698,6 @@ firstNameLink nameType simplexChannel simplexContact ni =
|
||||
NTPublicGroup -> simplexChannel
|
||||
NTContact -> simplexContact
|
||||
|
||||
-- | Map a resolver failure to the corresponding ChatError surfaced to the user.
|
||||
-- AUTH (NameNotRegistered) collapses to the same UX as a local-store miss, so
|
||||
-- the user can't tell from the error whether their device knew the name.
|
||||
-- Transport failures are forwarded through 'chatErrorAgent' so they reuse the
|
||||
-- existing agent-error reporting in the UI.
|
||||
resolveErrorToChatError :: SimplexNameInfo -> ResolveError -> ChatError
|
||||
resolveErrorToChatError ni = \case
|
||||
NameNotRegistered -> ChatError $ CESimplexNameNotFound ni
|
||||
ResolverUnavailable -> ChatError $ CESimplexNameResolverUnavailable ni
|
||||
ResolverTransport e -> chatErrorAgent e
|
||||
|
||||
-- | Best-effort comparison between an RSLV-resolved link (a 'Text' from the
|
||||
-- name record) and the peer's stored connection link. Both are normalized via
|
||||
-- 'strDecode' + 'strEncode' so scheme drift (simplex:/ vs https://simplex.chat)
|
||||
@@ -4787,37 +4718,31 @@ linksMatch resolved stored = case strDecode (encodeUtf8 resolved) :: Either Stri
|
||||
CLShort (CSLContact _ ct srv linkKey) ->
|
||||
strEncode (CSLContact SLSServer ct srv linkKey :: ConnShortLink 'CMContact)
|
||||
|
||||
-- | Resolves the chat row's simplex_name claim via RSLV and compares the
|
||||
-- resolved per-type link to the peer's stored connection link. On match,
|
||||
-- timestamps the contact/group row and emits CEvtSimplexNameVerified.
|
||||
-- On mismatch / RSLV failure, emits CEvtSimplexNameVerifyFailed.
|
||||
-- Throws CESimplexNameNotFound when the row has no claim to verify.
|
||||
apiVerifySimplexName :: User -> ChatRef -> CM ChatResponse
|
||||
apiVerifySimplexName user chatRef = do
|
||||
-- | Resolves the chat row's simplex_name claim via RSLV (the agent picks a
|
||||
-- names server) and compares the resolved per-type link to the peer's stored
|
||||
-- connection link. On match, timestamps the contact/group row. Returns
|
||||
-- CRSimplexNameVerified with the boolean result (mirrors CRConnectionVerified);
|
||||
-- resolver / agent failures propagate as the usual ChatErrorAgent.
|
||||
-- Throws a command error when the row has no claim to verify.
|
||||
apiVerifySimplexName :: User -> NetworkRequestMode -> ChatRef -> CM ChatResponse
|
||||
apiVerifySimplexName user nm chatRef = do
|
||||
cxt <- chatStoreCxt
|
||||
(claim, storedLink, persistVerified) <- loadClaimAndLink cxt
|
||||
let domain = (\SimplexNameInfo {nameDomain} -> nameDomain) claim
|
||||
nameType' = (\SimplexNameInfo {nameType} -> nameType) claim
|
||||
resolveOnUserServers user domain >>= \case
|
||||
Right NameRecord {nrSimplexContact, nrSimplexChannel} -> do
|
||||
let resolvedLinks = case nameType' of
|
||||
NTContact -> nrSimplexContact
|
||||
NTPublicGroup -> nrSimplexChannel
|
||||
let SimplexNameInfo {nameType = nameType', nameDomain = domain} = claim
|
||||
User {userId} = user
|
||||
a <- asks smpAgent
|
||||
NameRecord {nrSimplexContact, nrSimplexChannel} <-
|
||||
liftIO (runExceptT $ resolveSimplexName a nm userId domain) >>= either (throwError . chatErrorAgent) pure
|
||||
let resolvedLinks = case nameType' of
|
||||
NTContact -> nrSimplexContact
|
||||
NTPublicGroup -> nrSimplexChannel
|
||||
-- The peer's stored link verifies if it matches ANY advertised link
|
||||
-- (primary or fallback); an empty list never matches.
|
||||
if any (`linksMatch` storedLink) resolvedLinks
|
||||
then do
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' $ \db -> persistVerified db ts
|
||||
toView $ CEvtSimplexNameVerified user chatRef claim ts
|
||||
else toView $ CEvtSimplexNameVerifyFailed user chatRef claim SNVFLinkMismatch
|
||||
Left NameNotRegistered ->
|
||||
toView $ CEvtSimplexNameVerifyFailed user chatRef claim SNVFNameNotRegistered
|
||||
Left ResolverUnavailable ->
|
||||
throwChatError $ CESimplexNameResolverUnavailable claim
|
||||
Left (ResolverTransport e) ->
|
||||
toView $ CEvtSimplexNameVerifyFailed user chatRef claim (SNVFResolverError e)
|
||||
pure $ CRCmdOk (Just user)
|
||||
verified = any (`linksMatch` storedLink) resolvedLinks
|
||||
when verified $ do
|
||||
ts <- liftIO getCurrentTime
|
||||
withStore' $ \db -> persistVerified db ts
|
||||
pure $ CRSimplexNameVerified user chatRef claim verified
|
||||
where
|
||||
-- Returns the claim to verify, the peer's stored link, and a callback that
|
||||
-- persists the verified_at timestamp to the appropriate table. Throws a
|
||||
@@ -4840,42 +4765,6 @@ apiVerifySimplexName user chatRef = do
|
||||
pure (claim, lnk, \db ts -> setGroupSimplexNameVerifiedAt db user groupId ts)
|
||||
_ -> throwCmdError "APIVerifySimplexName supports only direct and group chat refs"
|
||||
|
||||
-- | Pure iteration logic for 'resolveOnUserServers'. Extracted so tests can
|
||||
-- supply a stub resolver without standing up a real agent / proxy.
|
||||
iterateResolvers ::
|
||||
Monad m =>
|
||||
[SMPServer] ->
|
||||
(SMPServer -> m (Either AgentErrorType NameRecord)) ->
|
||||
m (Either ResolveError NameRecord)
|
||||
iterateResolvers servers resolve = go servers
|
||||
where
|
||||
go [] = pure $ Left ResolverUnavailable
|
||||
go (srv : rest) =
|
||||
resolve srv >>= \case
|
||||
Right nr -> pure $ Right nr
|
||||
Left e
|
||||
| isNotRegistered e -> pure $ Left NameNotRegistered
|
||||
| isUnsupported e -> go rest
|
||||
| temporaryOrHostError e -> go rest
|
||||
| otherwise -> pure $ Left $ ResolverTransport e
|
||||
isNotRegistered = \case
|
||||
SMP _ SMP.AUTH -> True
|
||||
_ -> False
|
||||
-- A server that cannot resolve answers CMD UNKNOWN -- it predates RSLV (e.g.
|
||||
-- an old official server), and the client degraded RSLV to a no-op below
|
||||
-- namesSMPVersion so it never received the name -- or CMD PROHIBITED -- it
|
||||
-- speaks RSLV but has no resolver configured (names role off), so it did
|
||||
-- receive the name but cannot help. Either form may arrive directly or wrapped
|
||||
-- by a proxy. We skip it and try the next server; ResolverUnavailable is
|
||||
-- returned only when no server can resolve. A resolver-backed server's
|
||||
-- transient failure is INTERNAL (-> ResolverTransport), not handled here.
|
||||
isUnsupported = \case
|
||||
SMP _ (SMP.CMD SMP.UNKNOWN) -> True
|
||||
SMP _ (SMP.CMD SMP.PROHIBITED) -> True
|
||||
PROXY _ _ (ProxyProtocolError (SMP.CMD SMP.UNKNOWN)) -> True
|
||||
PROXY _ _ (ProxyProtocolError (SMP.CMD SMP.PROHIBITED)) -> True
|
||||
_ -> False
|
||||
|
||||
data ConnectViaContactResult
|
||||
= CVRConnectedContact Contact
|
||||
| CVRSentInvitation Connection (Maybe Profile)
|
||||
@@ -5811,9 +5700,9 @@ chatCommandP =
|
||||
srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',')
|
||||
where
|
||||
srvRoles = \case
|
||||
"off" -> Right $ ServerRoles False False
|
||||
"proxy" -> Right ServerRoles {storage = False, proxy = True}
|
||||
"storage" -> Right ServerRoles {storage = True, proxy = False}
|
||||
"off" -> Right $ ServerRoles False False False
|
||||
"proxy" -> Right ServerRoles {storage = False, proxy = True, names = False}
|
||||
"storage" -> Right ServerRoles {storage = True, proxy = False, names = False}
|
||||
"on" -> Right allRoles
|
||||
_ -> Left "bad ServerRoles"
|
||||
netCfgP = do
|
||||
|
||||
@@ -201,15 +201,6 @@ toggleNtf m ntfOn =
|
||||
forM_ (memberConnId m) $ \connId ->
|
||||
withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchAllErrors` eToView
|
||||
|
||||
-- | Emit CEvtSimplexNameConflict when an incoming claim displaced an older
|
||||
-- simplex_name binding. No-op when either piece of context is absent:
|
||||
-- claim Nothing = peer did not assert a simplex_name; displaced Nothing =
|
||||
-- no prior holder for the claim, so nothing was displaced.
|
||||
surfaceSimplexNameConflict :: User -> Maybe SimplexNameInfo -> Maybe ContactName -> SimplexNameConflictEntity -> ContactName -> CM ()
|
||||
surfaceSimplexNameConflict user claim_ displaced_ entity claimedBy =
|
||||
forM_ ((,) <$> claim_ <*> displaced_) $ \(ni, displaced) ->
|
||||
toView $ CEvtSimplexNameConflict user ni entity claimedBy displaced
|
||||
|
||||
prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> Maybe MsgScope -> ShowGroupAsSender -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))
|
||||
prepareGroupMsg db user g@GroupInfo {membership} msgScope showGroupAsSender mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = do
|
||||
(mc', quotedItem_) <- case (quotedItemId_, itemForwarded) of
|
||||
|
||||
@@ -2560,21 +2560,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
processContactProfileUpdate :: Contact -> Profile -> Bool -> CM Contact
|
||||
processContactProfileUpdate c@Contact {profile = lp} p' createItems
|
||||
| p /= p' = do
|
||||
(c', displaced_) <- withStore $ \db ->
|
||||
c' <- withStore $ \db ->
|
||||
if userTTL == rcvTTL
|
||||
then updateContactProfileWithConflict db user c p'
|
||||
then updateContactProfile db user c p'
|
||||
else do
|
||||
c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs'
|
||||
updateContactProfileWithConflict db user c' p'
|
||||
let Contact {contactId = ctId, localDisplayName = newLDN, simplexNameVerifiedAt = vAt} = c'
|
||||
surfaceSimplexNameConflict user p'SimplexName displaced_ SNCEContact newLDN
|
||||
-- Passive unverified warning: a non-empty incoming claim that the user
|
||||
-- has not verified (or whose prior verification was cleared by the
|
||||
-- claim transition in updateContactProfileWithConflict) should be
|
||||
-- surfaced so the UI can prompt the user to invoke APIVerifySimplexName.
|
||||
forM_ p'SimplexName $ \ni ->
|
||||
when (isNothing vAt) $
|
||||
toView $ CEvtSimplexNameUnverified user (ChatRef CTDirect ctId Nothing) ni
|
||||
updateContactProfile db user c' p'
|
||||
when (directOrUsed c' && createItems) $ do
|
||||
createProfileUpdatedItem c'
|
||||
lift $ createRcvFeatureItems user c c'
|
||||
@@ -2586,7 +2577,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
p = fromLocalProfile lp
|
||||
Contact {userPreferences = ctUserPrefs@Preferences {timedMessages = ctUserTMPref}} = c
|
||||
userTTL = prefParam $ getPreference SCFTimedMessages ctUserPrefs
|
||||
Profile {preferences = rcvPrefs_, simplexName = p'SimplexName} = p'
|
||||
Profile {preferences = rcvPrefs_} = p'
|
||||
rcvTTL = prefParam $ getPreference SCFTimedMessages rcvPrefs_
|
||||
ctUserPrefs' =
|
||||
let userDefault = getPreference SCFTimedMessages (fullPreferences user)
|
||||
@@ -2689,9 +2680,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
updateBusinessChatProfile gInfo
|
||||
case memberContactId of
|
||||
Nothing -> do
|
||||
(m', displaced_) <- withStore $ \db -> updateMemberProfileWithConflict db user m p'
|
||||
let GroupMember {localDisplayName = newLDN} = m'
|
||||
surfaceSimplexNameConflict user p'SimplexName displaced_ SNCEContact newLDN
|
||||
m' <- withStore $ \db -> updateMemberProfile db user m p'
|
||||
unless (muteEventInChannel gInfo m') $ do
|
||||
forM_ msgTs_ $ createProfileUpdatedItem m'
|
||||
toView $ CEvtGroupMemberUpdated user gInfo m m'
|
||||
@@ -2700,9 +2689,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
mCt <- withStore $ \db -> getContact db cxt user mContactId
|
||||
if canUpdateProfile mCt
|
||||
then do
|
||||
(m', ct', displaced_) <- withStore $ \db -> updateContactMemberProfileWithConflict db user m mCt p'
|
||||
let Contact {localDisplayName = newLDN} = ct'
|
||||
surfaceSimplexNameConflict user p'SimplexName displaced_ SNCEContact newLDN
|
||||
(m', ct') <- withStore $ \db -> updateContactMemberProfile db user m mCt p'
|
||||
unless (muteEventInChannel gInfo m') $ do
|
||||
forM_ msgTs_ $ createProfileUpdatedItem m'
|
||||
toView $ CEvtGroupMemberUpdated user gInfo m m'
|
||||
@@ -2719,7 +2706,6 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
pure m
|
||||
where
|
||||
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo
|
||||
Profile {simplexName = p'SimplexName} = p'
|
||||
updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of
|
||||
Just bc | isMainBusinessMember bc m -> do
|
||||
g' <- withStore $ \db -> updateGroupProfileFromMember db user g p'
|
||||
@@ -2965,10 +2951,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
-- to createDirectContact; after this point contacts.simplex_name is
|
||||
-- the source of truth.
|
||||
let Connection {simplexName} = conn'
|
||||
Profile {simplexName = pSimplexName} = p
|
||||
(ct, displaced_) <- withStore $ \db -> createDirectContact db cxt user conn' p simplexName
|
||||
let Contact {localDisplayName = newLDN} = ct
|
||||
surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN
|
||||
ct <- withStore $ \db -> createDirectContact db cxt user conn' p simplexName
|
||||
toView $ CEvtContactConnecting user ct
|
||||
pure (conn', Nothing)
|
||||
XGrpLinkInv glInv -> do
|
||||
@@ -3305,7 +3288,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' msgSigned
|
||||
|
||||
xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
|
||||
xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {publicGroup = pg}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {publicGroup = pg', simplexName = p'GroupSimplexName} msg@RcvMessage {msgSigned} brokerTs
|
||||
xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {publicGroup = pg}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {publicGroup = pg'} msg@RcvMessage {msgSigned} brokerTs
|
||||
| memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" $> Nothing
|
||||
| let pgId = fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId),
|
||||
useRelays' g && (isNothing pg' || pgId pg' /= pgId pg) = messageError "x.grp.info: publicGroupId mismatch for channel" $> Nothing
|
||||
@@ -3313,13 +3296,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
| otherwise = do
|
||||
case businessChat of
|
||||
Nothing -> unless (p == p') $ do
|
||||
(g', displaced_) <- withStore $ \db -> updateGroupProfileWithConflict db user g p'
|
||||
let GroupInfo {groupId = gId, localDisplayName = newLDN, simplexNameVerifiedAt = vAt} = g'
|
||||
surfaceSimplexNameConflict user p'GroupSimplexName displaced_ SNCEGroup newLDN
|
||||
-- Passive unverified warning, mirrors processContactProfileUpdate.
|
||||
forM_ p'GroupSimplexName $ \ni ->
|
||||
when (isNothing vAt) $
|
||||
toView $ CEvtSimplexNameUnverified user (ChatRef CTGroup gId Nothing) ni
|
||||
g' <- withStore $ \db -> updateGroupProfile db user g p'
|
||||
(g'', m', scopeInfo) <- mkGroupChatScope g' m
|
||||
toView $ CEvtGroupUpdated user g g'' (Just m') msgSigned
|
||||
let cd = CDGroupRcv g'' scopeInfo m'
|
||||
|
||||
@@ -39,8 +39,8 @@ operatorFlux =
|
||||
serverDomains = ["simplexonflux.com"],
|
||||
conditionsAcceptance = CARequired Nothing,
|
||||
enabled = True,
|
||||
smpRoles = ServerRoles {storage = False, proxy = True},
|
||||
xftpRoles = ServerRoles {storage = False, proxy = True}
|
||||
smpRoles = ServerRoles {storage = False, proxy = True, names = False},
|
||||
xftpRoles = ServerRoles {storage = False, proxy = True, names = False}
|
||||
}
|
||||
|
||||
-- Please note: if any servers are removed from the lists below, they MUST be added here.
|
||||
|
||||
@@ -53,7 +53,6 @@ module Simplex.Chat.Store.Direct
|
||||
getContactIdByName,
|
||||
getContactIdBySimplexName,
|
||||
updateContactProfile,
|
||||
updateContactProfileWithConflict,
|
||||
setContactSimplexNameVerifiedAt,
|
||||
updateContactUserPreferences,
|
||||
updateContactAlias,
|
||||
@@ -401,18 +400,13 @@ createIncognitoProfile db User {userId} p = do
|
||||
createdAt <- getCurrentTime
|
||||
createIncognitoProfile_ db userId createdAt p
|
||||
|
||||
-- | Returns (contact, displaced) — displaced is Just the display_name of a
|
||||
-- contact_profiles row whose peer-claimed simplex_name was cleared to make
|
||||
-- room for the new contact's claim, so the caller can emit
|
||||
-- CEvtSimplexNameConflict.
|
||||
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName)
|
||||
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO Contact
|
||||
createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId simplexName = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
|
||||
ctUserPreferences = newContactUserPrefs user p
|
||||
(contactId, displaced) <- createContact_ db user p ctUserPreferences prepared "" currentTs simplexName
|
||||
ct <- getContact db cxt user contactId
|
||||
pure (ct, displaced)
|
||||
contactId <- createContact_ db user p ctUserPreferences prepared "" currentTs simplexName
|
||||
getContact db cxt user contactId
|
||||
|
||||
updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact
|
||||
updatePreparedContactUser
|
||||
@@ -452,15 +446,13 @@ updatePreparedContactUser
|
||||
safeDeleteLDN db user oldLDN
|
||||
getContact db cxt newUser contactId
|
||||
|
||||
-- | Returns (contact, displaced) — see createPreparedContact for displaced.
|
||||
createDirectContact :: DB.Connection -> StoreCxt -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName)
|
||||
createDirectContact :: DB.Connection -> StoreCxt -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO Contact
|
||||
createDirectContact db cxt user Connection {connId, localAlias} p simplexName = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let ctUserPreferences = newContactUserPrefs user p
|
||||
(contactId, displaced) <- createContact_ db user p ctUserPreferences Nothing localAlias currentTs simplexName
|
||||
contactId <- createContact_ db user p ctUserPreferences Nothing localAlias currentTs simplexName
|
||||
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId)
|
||||
ct <- getContact db cxt user contactId
|
||||
pure (ct, displaced)
|
||||
getContact db cxt user contactId
|
||||
|
||||
deleteContactConnections :: DB.Connection -> User -> Contact -> IO ()
|
||||
deleteContactConnections db User {userId} Contact {contactId} = do
|
||||
@@ -566,33 +558,29 @@ deleteUnusedProfile_ db userId profileId =
|
||||
:. (userId, profileId, userId, profileId, profileId)
|
||||
)
|
||||
|
||||
updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact
|
||||
updateContactProfile db user c p' = fst <$> updateContactProfileWithConflict db user c p'
|
||||
|
||||
-- | Like updateContactProfile but additionally clears the simplex_name on any
|
||||
-- other contact_profiles row in the same user that already holds the same
|
||||
-- (user_id, simplex_name) — returning that row's display_name so the caller
|
||||
-- can emit CEvtSimplexNameConflict. Used by the incoming-XInfo path; local
|
||||
-- updates that don't expect conflicts can continue to use updateContactProfile.
|
||||
-- | Updates the contact profile, also clearing the simplex_name on any other
|
||||
-- contact_profiles row in the same user that already holds the same
|
||||
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE
|
||||
-- index.
|
||||
--
|
||||
-- Also clears contacts.simplex_name_verified_at when the peer's simplex_name
|
||||
-- claim changes (any value transition, including Nothing<->Just): the prior
|
||||
-- verification was tied to the prior claim and must be re-issued by the user.
|
||||
updateContactProfileWithConflict :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO (Contact, Maybe ContactName)
|
||||
updateContactProfileWithConflict db user@User {userId} c p'
|
||||
updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact
|
||||
updateContactProfile db user@User {userId} c p'
|
||||
| displayName == newName = do
|
||||
displaced <- liftIO $ clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
liftIO $ clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
liftIO $ updateContactProfile_ db userId profileId p'
|
||||
liftIO clearVerifiedAtIfClaimChanged
|
||||
pure (c' {profile, mergedPreferences}, displaced)
|
||||
pure $ c' {profile, mergedPreferences}
|
||||
| otherwise =
|
||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||
currentTs <- getCurrentTime
|
||||
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
updateContactProfile_' db userId profileId p' currentTs
|
||||
updateContactLDN_ db user contactId localDisplayName ldn currentTs
|
||||
clearVerifiedAtIfClaimChanged
|
||||
pure $ Right (c' {localDisplayName = ldn, profile, mergedPreferences}, displaced)
|
||||
pure $ Right c' {localDisplayName = ldn, profile, mergedPreferences}
|
||||
where
|
||||
Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias, simplexName = prevClaim}, userPreferences} = c
|
||||
Profile {displayName = newName, simplexName = profileSimplexName, preferences} = p'
|
||||
@@ -606,7 +594,7 @@ updateContactProfileWithConflict db user@User {userId} c p'
|
||||
|
||||
-- | Records that the user successfully RSLV-verified the peer's simplex_name
|
||||
-- claim against the contact's stored connection link. Cleared back to NULL by
|
||||
-- updateContactProfileWithConflict whenever the peer's claim transitions.
|
||||
-- updateContactProfile whenever the peer's claim transitions.
|
||||
setContactSimplexNameVerifiedAt :: DB.Connection -> User -> ContactId -> UTCTime -> IO ()
|
||||
setContactSimplexNameVerifiedAt db User {userId} contactId ts =
|
||||
DB.execute
|
||||
|
||||
@@ -46,7 +46,6 @@ module Simplex.Chat.Store.Groups
|
||||
getGroupViaShortLinkToConnect,
|
||||
getGroupInfoByGroupLinkHash,
|
||||
updateGroupProfile,
|
||||
updateGroupProfileWithConflict,
|
||||
setGroupSimplexNameVerifiedAt,
|
||||
clearConflictingGroupProfileSimplexName_,
|
||||
updateGroupPreferences,
|
||||
@@ -167,9 +166,7 @@ module Simplex.Chat.Store.Groups
|
||||
setMemberContactStartedConnection,
|
||||
resetMemberContactFields,
|
||||
updateMemberProfile,
|
||||
updateMemberProfileWithConflict,
|
||||
updateContactMemberProfile,
|
||||
updateContactMemberProfileWithConflict,
|
||||
getXGrpLinkMemReceived,
|
||||
setXGrpLinkMemReceived,
|
||||
createNewUnknownGroupMember,
|
||||
@@ -2388,36 +2385,32 @@ createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Version
|
||||
createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode =
|
||||
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff Nothing
|
||||
|
||||
-- | Updates the group profile, also clearing the simplex_name on any other
|
||||
-- group_profiles row (for the same user) that already holds the same
|
||||
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE index.
|
||||
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
|
||||
updateGroupProfile db user g p' = fst <$> updateGroupProfileWithConflict db user g p'
|
||||
|
||||
-- | Like updateGroupProfile but additionally clears the simplex_name on any
|
||||
-- other group_profiles row (for the same user) that already holds the same
|
||||
-- (user_id, simplex_name) — returning that row's display_name so the caller
|
||||
-- can emit CEvtSimplexNameConflict. Used by the XGrpInfo path.
|
||||
updateGroupProfileWithConflict :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO (GroupInfo, Maybe GroupName)
|
||||
updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, simplexName = prevClaim}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, simplexName, groupPreferences, memberAdmission}
|
||||
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, simplexName = prevClaim}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, simplexName, groupPreferences, memberAdmission}
|
||||
| displayName == newName = liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
profileId_ <- getGroupProfileId_
|
||||
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
|
||||
clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
|
||||
updateGroupProfile_ currentTs
|
||||
clearVerifiedAtIfClaimChanged
|
||||
pure ((g' :: GroupInfo) {groupProfile = p', fullGroupPreferences}, displaced)
|
||||
pure $ (g' :: GroupInfo) {groupProfile = p', fullGroupPreferences}
|
||||
| otherwise =
|
||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||
currentTs <- getCurrentTime
|
||||
profileId_ <- getGroupProfileId_
|
||||
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
|
||||
clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
|
||||
updateGroupProfile_ currentTs
|
||||
updateGroup_ ldn currentTs
|
||||
clearVerifiedAtIfClaimChanged
|
||||
pure $ Right ((g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}, displaced)
|
||||
pure $ Right $ (g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}
|
||||
where
|
||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||
claimChanged = prevClaim /= simplexName
|
||||
g' = if claimChanged then (g :: GroupInfo) {simplexNameVerifiedAt = Nothing} else g
|
||||
-- Mirrors updateContactProfileWithConflict: clear the verification when
|
||||
-- Mirrors updateContactProfile: clear the verification when
|
||||
-- the peer's claim transitions to/from/between values; prior verification
|
||||
-- was bound to the prior claim.
|
||||
clearVerifiedAtIfClaimChanged =
|
||||
@@ -2463,30 +2456,26 @@ updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, local
|
||||
-- (rather than derived from groupId via a NOT IN subquery) because
|
||||
-- groups.group_profile_id is ON DELETE SET NULL, and NOT IN (NULL)
|
||||
-- evaluates to UNKNOWN — which would silently no-op the clear.
|
||||
clearConflictingGroupProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO (Maybe GroupName)
|
||||
clearConflictingGroupProfileSimplexName_ _ _ _ Nothing = pure Nothing
|
||||
clearConflictingGroupProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO ()
|
||||
clearConflictingGroupProfileSimplexName_ _ _ _ Nothing = pure ()
|
||||
clearConflictingGroupProfileSimplexName_ db userId Nothing (Just simplexName) =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ?
|
||||
RETURNING display_name
|
||||
|]
|
||||
(userId, simplexName)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ?
|
||||
|]
|
||||
(userId, simplexName)
|
||||
clearConflictingGroupProfileSimplexName_ db userId (Just profileId) (Just simplexName) =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ? AND group_profile_id <> ?
|
||||
RETURNING display_name
|
||||
|]
|
||||
(userId, simplexName, profileId)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ? AND group_profile_id <> ?
|
||||
|]
|
||||
(userId, simplexName, profileId)
|
||||
|
||||
-- | Mirror of setContactSimplexNameVerifiedAt for groups.
|
||||
setGroupSimplexNameVerifiedAt :: DB.Connection -> User -> GroupId -> UTCTime -> IO ()
|
||||
@@ -3121,57 +3110,49 @@ setMemberContactStartedConnection db Contact {contactId} = do
|
||||
"UPDATE contacts SET grp_direct_inv_started_connection = ?, updated_at = ? WHERE contact_id = ?"
|
||||
(BI True, currentTs, contactId)
|
||||
|
||||
-- | Updates the member profile, also clearing the simplex_name on any other
|
||||
-- contact_profiles row in the same user that already holds the same
|
||||
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE index.
|
||||
updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
|
||||
updateMemberProfile db user m p' = fst <$> updateMemberProfileWithConflict db user m p'
|
||||
|
||||
-- | Like updateMemberProfile but additionally clears the simplex_name on any
|
||||
-- other contact_profiles row in the same user that already holds the same
|
||||
-- (user_id, simplex_name) — returning that row's display_name so the caller
|
||||
-- can emit CEvtSimplexNameConflict. Used by the incoming XInfo (member) path.
|
||||
updateMemberProfileWithConflict :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO (GroupMember, Maybe ContactName)
|
||||
updateMemberProfileWithConflict db user@User {userId} m p'
|
||||
updateMemberProfile db user@User {userId} m p'
|
||||
| displayName == newName = liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
updateMemberContactProfileReset_' db userId profileId p' currentTs
|
||||
pure (m {memberProfile = profile}, displaced)
|
||||
pure m {memberProfile = profile}
|
||||
| otherwise =
|
||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||
currentTs <- getCurrentTime
|
||||
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
updateMemberContactProfileReset_' db userId profileId p' currentTs
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?"
|
||||
(ldn, currentTs, userId, groupMemberId)
|
||||
safeDeleteLDN db user localDisplayName
|
||||
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, displaced)
|
||||
pure $ Right m {localDisplayName = ldn, memberProfile = profile}
|
||||
where
|
||||
GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
|
||||
Profile {displayName = newName, simplexName = profileSimplexName} = p'
|
||||
profile = toLocalProfile profileId p' localAlias
|
||||
|
||||
-- | Updates the member's contact profile, also clearing the simplex_name on any
|
||||
-- other contact_profiles row in the same user that already holds the same
|
||||
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE index.
|
||||
updateContactMemberProfile :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, Contact)
|
||||
updateContactMemberProfile db user m ct p' = (\(m', ct', _) -> (m', ct')) <$> updateContactMemberProfileWithConflict db user m ct p'
|
||||
|
||||
-- | Like updateContactMemberProfile but additionally clears the simplex_name
|
||||
-- on any other contact_profiles row in the same user that already holds the
|
||||
-- same (user_id, simplex_name) — returning that row's display_name so the
|
||||
-- caller can emit CEvtSimplexNameConflict.
|
||||
updateContactMemberProfileWithConflict :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, Contact, Maybe ContactName)
|
||||
updateContactMemberProfileWithConflict db user@User {userId} m ct@Contact {contactId} p'
|
||||
updateContactMemberProfile db user@User {userId} m ct@Contact {contactId} p'
|
||||
| displayName == newName = liftIO $ do
|
||||
currentTs <- getCurrentTime
|
||||
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
updateMemberContactProfile_' db userId profileId p' currentTs
|
||||
pure (m {memberProfile = profile}, ct {profile} :: Contact, displaced)
|
||||
pure (m {memberProfile = profile}, ct {profile} :: Contact)
|
||||
| otherwise =
|
||||
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
|
||||
currentTs <- getCurrentTime
|
||||
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
|
||||
updateMemberContactProfile_' db userId profileId p' currentTs
|
||||
updateContactLDN_ db user contactId localDisplayName ldn currentTs
|
||||
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact, displaced)
|
||||
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact)
|
||||
where
|
||||
GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
|
||||
Profile {displayName = newName, simplexName = profileSimplexName} = p'
|
||||
|
||||
@@ -38,6 +38,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260604_simplex_name_profiles
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260606_simplex_name_verified
|
||||
import Simplex.Chat.Store.Postgres.Migrations.M20260612_smp_role_names
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Text, Maybe Text)]
|
||||
@@ -75,7 +76,8 @@ schemaMigrations =
|
||||
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
|
||||
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name),
|
||||
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles),
|
||||
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified)
|
||||
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified),
|
||||
("20260612_smp_role_names", m20260612_smp_role_names, Just down_m20260612_smp_role_names)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Postgres.Migrations.M20260612_smp_role_names where
|
||||
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Text.RawString.QQ (r)
|
||||
|
||||
m20260612_smp_role_names :: Text
|
||||
m20260612_smp_role_names =
|
||||
T.pack
|
||||
[r|
|
||||
ALTER TABLE server_operators ADD COLUMN smp_role_names SMALLINT NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex';
|
||||
|]
|
||||
|
||||
down_m20260612_smp_role_names :: Text
|
||||
down_m20260612_smp_role_names =
|
||||
T.pack
|
||||
[r|
|
||||
ALTER TABLE server_operators DROP COLUMN smp_role_names;
|
||||
|]
|
||||
@@ -1334,7 +1334,8 @@ CREATE TABLE test_chat_schema.server_operators (
|
||||
xftp_role_storage smallint DEFAULT 1 NOT NULL,
|
||||
xftp_role_proxy smallint DEFAULT 1 NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
smp_role_names smallint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -719,10 +719,10 @@ updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles,
|
||||
db
|
||||
[sql|
|
||||
UPDATE server_operators
|
||||
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
|
||||
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
|
||||
WHERE server_operator_id = ?
|
||||
|]
|
||||
(BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId)
|
||||
(BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId)
|
||||
|
||||
getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [(Maybe PresetOperator, Maybe ServerOperator)]
|
||||
getUpdateServerOperators db presetOps newUser = do
|
||||
@@ -757,20 +757,20 @@ getUpdateServerOperators db presetOps newUser = do
|
||||
db
|
||||
[sql|
|
||||
UPDATE server_operators
|
||||
SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?
|
||||
SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?
|
||||
WHERE server_operator_id = ?
|
||||
|]
|
||||
(tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId)
|
||||
(tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId)
|
||||
insertOperator :: NewServerOperator -> IO ServerOperator
|
||||
insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = do
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO server_operators
|
||||
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles))
|
||||
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles))
|
||||
opId <- insertedRowId db
|
||||
pure op {operatorId = DBEntityId opId}
|
||||
autoAcceptConditions op UsageConditions {conditionsCommit} now =
|
||||
@@ -781,14 +781,14 @@ serverOperatorQuery :: Query
|
||||
serverOperatorQuery =
|
||||
[sql|
|
||||
SELECT server_operator_id, server_operator_tag, trade_name, legal_name,
|
||||
server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy
|
||||
server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy
|
||||
FROM server_operators
|
||||
|]
|
||||
|
||||
getServerOperators_ :: DB.Connection -> IO [ServerOperator]
|
||||
getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery
|
||||
|
||||
toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator
|
||||
toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator
|
||||
toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI enabled) :. smpRoles' :. xftpRoles') =
|
||||
ServerOperator
|
||||
{ operatorId,
|
||||
@@ -798,11 +798,13 @@ toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI en
|
||||
serverDomains = T.splitOn "," domains,
|
||||
conditionsAcceptance = CARequired Nothing,
|
||||
enabled,
|
||||
smpRoles = serverRoles smpRoles',
|
||||
xftpRoles = serverRoles xftpRoles'
|
||||
smpRoles = serverRolesSMP smpRoles',
|
||||
xftpRoles = serverRolesXFTP xftpRoles'
|
||||
}
|
||||
where
|
||||
serverRoles (BI storage, BI proxy) = ServerRoles {storage, proxy}
|
||||
serverRolesSMP (BI storage, BI proxy, BI names) = ServerRoles {storage, proxy, names}
|
||||
-- XFTP has no names role; the column is SMP-only.
|
||||
serverRolesXFTP (BI storage, BI proxy) = ServerRoles {storage, proxy, names = False}
|
||||
|
||||
getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance
|
||||
getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do
|
||||
|
||||
@@ -161,6 +161,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260604_simplex_name_profiles
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260606_simplex_name_verified
|
||||
import Simplex.Chat.Store.SQLite.Migrations.M20260612_smp_role_names
|
||||
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
|
||||
|
||||
schemaMigrations :: [(String, Query, Maybe Query)]
|
||||
@@ -321,7 +322,8 @@ schemaMigrations =
|
||||
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
|
||||
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name),
|
||||
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles),
|
||||
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified)
|
||||
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified),
|
||||
("20260612_smp_role_names", m20260612_smp_role_names, Just down_m20260612_smp_role_names)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -10,7 +10,7 @@ import Database.SQLite.Simple.QQ (sql)
|
||||
-- resolves (via RSLV) to the link stored locally for the contact/group.
|
||||
-- NULL means the claim is unverified and the UI should show an indicator.
|
||||
-- The column is cleared back to NULL whenever the simplex_name claim changes
|
||||
-- (updateContactProfileWithConflict / updateGroupProfileWithConflict).
|
||||
-- (updateContactProfile / updateGroupProfile).
|
||||
m20260606_simplex_name_verified :: Query
|
||||
m20260606_simplex_name_verified =
|
||||
[sql|
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.SQLite.Migrations.M20260612_smp_role_names where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20260612_smp_role_names :: Query
|
||||
m20260612_smp_role_names =
|
||||
[sql|
|
||||
ALTER TABLE server_operators ADD COLUMN smp_role_names INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex';
|
||||
|]
|
||||
|
||||
down_m20260612_smp_role_names :: Query
|
||||
down_m20260612_smp_role_names =
|
||||
[sql|
|
||||
ALTER TABLE server_operators DROP COLUMN smp_role_names;
|
||||
|]
|
||||
@@ -687,6 +687,8 @@ CREATE TABLE server_operators(
|
||||
xftp_role_proxy INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
|
||||
,
|
||||
smp_role_names INTEGER NOT NULL DEFAULT 0
|
||||
) STRICT;
|
||||
CREATE TABLE usage_conditions(
|
||||
usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
@@ -433,52 +433,47 @@ createContact db user profile = do
|
||||
-- | Clears simplex_name on any other contact_profiles row that holds the same
|
||||
-- (user_id, simplex_name) so a subsequent UPDATE/INSERT setting that value
|
||||
-- won't trip the partial UNIQUE index. Pass the profileId being updated to
|
||||
-- exclude self; pass Nothing for the pre-INSERT case. Returns the displaced
|
||||
-- row's display_name when a conflict was resolved, for the caller to surface
|
||||
-- as CEvtSimplexNameConflict. Newer-claim-wins matches RSLV semantics: the
|
||||
-- latest broadcast is the canonical assignment.
|
||||
-- exclude self; pass Nothing for the pre-INSERT case. Newer-claim-wins matches
|
||||
-- RSLV semantics: the latest broadcast is the canonical assignment. The partial
|
||||
-- UNIQUE index on (user_id, simplex_name) requires the prior holder be cleared
|
||||
-- before the new row can set the name.
|
||||
--
|
||||
-- Cross-table collision with group_profiles.simplex_name is structurally
|
||||
-- impossible: strEncode SimplexNameInfo prefixes contact names with '@' and
|
||||
-- group names with '#', so the encoded bytes stored in the column never
|
||||
-- overlap between the two tables.
|
||||
clearConflictingContactProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO (Maybe ContactName)
|
||||
clearConflictingContactProfileSimplexName_ _ _ _ Nothing = pure Nothing
|
||||
clearConflictingContactProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO ()
|
||||
clearConflictingContactProfileSimplexName_ _ _ _ Nothing = pure ()
|
||||
clearConflictingContactProfileSimplexName_ db userId Nothing (Just simplexName) =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ?
|
||||
RETURNING display_name
|
||||
|]
|
||||
(userId, simplexName)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ?
|
||||
|]
|
||||
(userId, simplexName)
|
||||
clearConflictingContactProfileSimplexName_ db userId (Just profileId) (Just simplexName) =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ? AND contact_profile_id <> ?
|
||||
RETURNING display_name
|
||||
|]
|
||||
(userId, simplexName, profileId)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE contact_profiles
|
||||
SET simplex_name = NULL
|
||||
WHERE user_id = ? AND simplex_name = ? AND contact_profile_id <> ?
|
||||
|]
|
||||
(userId, simplexName, profileId)
|
||||
|
||||
-- | Inserts a new contact and its profile. Returns the new contactId and,
|
||||
-- if the peer-claimed Profile.simplexName collided with an existing row
|
||||
-- (the partial UNIQUE index on contact_profiles.(user_id, simplex_name)),
|
||||
-- the display_name of the displaced row — newer-claim-wins. The caller
|
||||
-- is responsible for emitting CEvtSimplexNameConflict on displacement.
|
||||
createContact_ :: DB.Connection -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> Maybe SimplexNameInfo -> ExceptT StoreError IO (ContactId, Maybe ContactName)
|
||||
-- | Inserts a new contact and its profile, returning the new contactId. A
|
||||
-- peer-claimed Profile.simplexName that collides with an existing row (the
|
||||
-- partial UNIQUE index on contact_profiles.(user_id, simplex_name)) displaces
|
||||
-- the prior holder's name — newer-claim-wins.
|
||||
createContact_ :: DB.Connection -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> Maybe SimplexNameInfo -> ExceptT StoreError IO ContactId
|
||||
createContact_ db User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, simplexName = profileSimplexName, peerType, preferences} ctUserPreferences prepared localAlias currentTs simplexName =
|
||||
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
|
||||
-- Clear any existing peer claim on the same simplex_name before INSERT
|
||||
-- so the partial UNIQUE index doesn't reject the new row. Pass Nothing
|
||||
-- as the excluded profileId — there's no self-row yet.
|
||||
displaced <- clearConflictingContactProfileSimplexName_ db userId Nothing profileSimplexName
|
||||
clearConflictingContactProfileSimplexName_ db userId Nothing profileSimplexName
|
||||
DB.execute
|
||||
db
|
||||
"INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, simplex_name, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
@@ -489,7 +484,7 @@ createContact_ db User {userId} Profile {displayName, fullName, shortDescr, imag
|
||||
"INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id, simplex_name) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
((profileId, ctUserPreferences, ldn, userId, currentTs, currentTs, currentTs, BI True) :. toPreparedContactRow prepared :. Only simplexName)
|
||||
contactId <- insertedRowId db
|
||||
pure $ Right (contactId, displaced)
|
||||
pure $ Right contactId
|
||||
|
||||
newContactUserPrefs :: User -> Profile -> Preferences
|
||||
newContactUserPrefs User {fullPreferences = FullPreferences {timedMessages = userTM}} Profile {preferences} =
|
||||
|
||||
@@ -214,7 +214,7 @@ data Contact = Contact
|
||||
-- | Timestamp of the most recent successful RSLV verification of the peer's
|
||||
-- simplex_name claim against this contact's connection link. NULL means the
|
||||
-- claim is unverified (UI should surface an indicator). Cleared back to NULL
|
||||
-- whenever simplex_name changes in updateContactProfileWithConflict.
|
||||
-- whenever simplex_name changes in updateContactProfile.
|
||||
simplexNameVerifiedAt :: Maybe UTCTime
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
@@ -498,7 +498,7 @@ data GroupInfo = GroupInfo
|
||||
groupKeys :: Maybe GroupKeys,
|
||||
simplexName :: Maybe SimplexNameInfo,
|
||||
-- | See 'Contact.simplexNameVerifiedAt'. Verified against the channel link
|
||||
-- stored for the group; cleared by updateGroupProfileWithConflict.
|
||||
-- stored for the group; cleared by updateGroupProfile.
|
||||
simplexNameVerifiedAt :: Maybe UTCTime
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -146,6 +146,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
|
||||
CRContactRatchetSyncStarted {} -> ["connection synchronization started"]
|
||||
CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"]
|
||||
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
|
||||
CRSimplexNameVerified u _chatRef ni verified -> ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> if verified then " verified" else " not verified"]
|
||||
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
|
||||
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
|
||||
CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView
|
||||
@@ -555,28 +556,6 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
|
||||
TEContactVerificationReset u ct -> ttyUser u $ viewContactVerificationReset ct
|
||||
TEGroupMemberVerificationReset u g m -> ttyUser u $ viewGroupMemberVerificationReset g m
|
||||
CEvtCustomChatEvent u r -> ttyUser' u $ map plain $ T.lines r
|
||||
CEvtSimplexNameConflict u ni entity claimedBy displacedFrom ->
|
||||
ttyUser u
|
||||
[ "simplex name "
|
||||
<> plain (shortNameInfoStr ni)
|
||||
<> " now claimed by "
|
||||
<> plain (entityLabel entity claimedBy)
|
||||
<> ", was "
|
||||
<> plain (entityLabel entity displacedFrom)
|
||||
]
|
||||
where
|
||||
entityLabel SNCEContact n = "@" <> n
|
||||
entityLabel SNCEGroup n = "#" <> n
|
||||
CEvtSimplexNameVerified u _chatRef ni _ts ->
|
||||
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " verified"]
|
||||
CEvtSimplexNameVerifyFailed u _chatRef ni r ->
|
||||
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " verification failed: " <> reason r]
|
||||
where
|
||||
reason SNVFLinkMismatch = "link mismatch"
|
||||
reason SNVFNameNotRegistered = "name not registered"
|
||||
reason (SNVFResolverError e) = "resolver error: " <> sShow e
|
||||
CEvtSimplexNameUnverified u _chatRef ni ->
|
||||
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " is unverified"]
|
||||
where
|
||||
ttyUser :: User -> [StyledString] -> [StyledString]
|
||||
ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss
|
||||
@@ -2649,7 +2628,6 @@ viewChatError isCmd logLevel testView = \case
|
||||
CEInvalidConnReq -> viewInvalidConnReq
|
||||
CESimplexNameNotFound ni -> ["no contact or group with simplex name " <> plain (shortNameInfoStr ni)]
|
||||
CESimplexNameUnprepared ni -> [plain (shortNameInfoStr ni) <> " is a known contact/group but has no link to reconnect via"]
|
||||
CESimplexNameResolverUnavailable ni -> ["no SMP server in your list supports name resolution for " <> plain (shortNameInfoStr ni)]
|
||||
CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr]
|
||||
CEInvalidChatMessage Connection {connId} msgMeta_ msg e ->
|
||||
[ plain $
|
||||
|
||||
@@ -582,6 +582,7 @@ smpServerCfg =
|
||||
allowSMPProxy = True,
|
||||
serverClientConcurrency = 16,
|
||||
namesConfig = Nothing,
|
||||
namesResolverCall_ = Nothing,
|
||||
information = Nothing,
|
||||
startOptions = StartOptions {maintenance = False, compactLog = False, logLevel = LogError, skipWarnings = False, confirmMigrations = MCYesUp}
|
||||
}
|
||||
|
||||
+9
-157
@@ -5,106 +5,25 @@
|
||||
|
||||
module ResolveNameTests (resolveNameTests) where
|
||||
|
||||
import Data.Functor.Identity (Identity (..))
|
||||
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
|
||||
import qualified Data.Map.Strict as M
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text.Encoding as T
|
||||
import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..))
|
||||
import Simplex.Chat.Library.Commands (ResolveError (..), firstNameLink, iterateResolvers, linksMatch, resolveErrorToChatError)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), AgentErrorType (..), ConnShortLink, ConnectionLink (..), ConnectionMode (..), SConnectionMode (..), SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
|
||||
import Simplex.Messaging.Client (ProxyClientError (..))
|
||||
import Simplex.Chat.Library.Commands (firstNameLink, linksMatch)
|
||||
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnShortLink, ConnectionLink (..), ConnectionMode (..), SConnectionMode (..), SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..))
|
||||
import Simplex.Messaging.Encoding.String (strDecode)
|
||||
import Simplex.Messaging.Protocol (BrokerErrorType (..), NameRecord (..), NetworkError (..), SMPServer, mkNameOwner, pattern SMPServer)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Test.Hspec
|
||||
|
||||
-- Name resolution and verification are owned by the agent (resolveSimplexName),
|
||||
-- and failures flow through ChatErrorAgent — there is no chat-side iteration or
|
||||
-- error-translation layer to test. These specs cover the two pure helpers that
|
||||
-- remain in the chat layer: firstNameLink (link selection) and linksMatch
|
||||
-- (verification comparison).
|
||||
resolveNameTests :: Spec
|
||||
resolveNameTests = do
|
||||
-- iterateResolvers is the testable core of resolveOnUserServers: it walks
|
||||
-- a list of candidate SMP servers, querying a resolver per server. Transport
|
||||
-- failures (NETWORK / TIMEOUT / host-unreachable) and unsupported servers
|
||||
-- (CMD UNKNOWN / PROHIBITED — relays that don't speak RSLV) both fall through
|
||||
-- to the next server; an unsupported relay never received the name (the client
|
||||
-- degrades RSLV below namesSMPVersion), so skipping it discloses nothing. A
|
||||
-- definitive answer from a name-capable relay — hit, AUTH, or any other definite
|
||||
-- error — stops iteration so the name is not broadcast to every operator.
|
||||
describe "iterateResolvers" $ do
|
||||
it "returns the first server's NameRecord on hit" $ do
|
||||
let r = runIdentity $ iterateResolvers [srv1, srv2] $ \_ -> pure $ Right sampleRecord
|
||||
r `shouldBe` Right sampleRecord
|
||||
it "skips an unsupported (CMD PROHIBITED) server and uses the next server's hit" $ do
|
||||
callsRef <- newIORef []
|
||||
r <- iterateResolvers [srv1, srv2] (recording callsRef stubProhibitedThenHit)
|
||||
r `shouldBe` Right sampleRecord
|
||||
-- both servers consulted: srv1 doesn't speak RSLV, so we fall through to
|
||||
-- srv2. srv1 never received the name (RSLV degrades below namesSMPVersion).
|
||||
readIORef callsRef `shouldReturn` [srv1, srv2]
|
||||
it "returns ResolverUnavailable when every server is unsupported" $ do
|
||||
callsRef <- newIORef []
|
||||
r <- iterateResolvers [srv1, srv2] (recording callsRef (\_ -> pure $ Left prohibitedErr))
|
||||
r `shouldBe` Left ResolverUnavailable
|
||||
-- all servers consulted, none could resolve.
|
||||
readIORef callsRef `shouldReturn` [srv1, srv2]
|
||||
it "treats AUTH as definitive NameNotRegistered and stops iteration" $ do
|
||||
callsRef <- newIORef []
|
||||
r <- iterateResolvers [srv1, srv2] (recording callsRef stubAuthThenHit)
|
||||
r `shouldBe` Left NameNotRegistered
|
||||
-- second server must NOT be consulted: AUTH is authoritative
|
||||
readIORef callsRef `shouldReturn` [srv1]
|
||||
it "stops on a definite non-transport error and returns ResolverTransport" $ do
|
||||
callsRef <- newIORef []
|
||||
r <- iterateResolvers [srv1, srv2] (recording callsRef (\_ -> pure $ Left otherDefiniteErr))
|
||||
case r of
|
||||
Left (ResolverTransport e) -> e `shouldBe` otherDefiniteErr
|
||||
other -> expectationFailure $ "expected ResolverTransport, got " <> show other
|
||||
-- second server must NOT be consulted: definite error means the server
|
||||
-- answered, so iterating would leak the queried name.
|
||||
readIORef callsRef `shouldReturn` [srv1]
|
||||
it "surfaces a resolver-backend INTERNAL as ResolverTransport, not NotFound" $ do
|
||||
callsRef <- newIORef []
|
||||
r <- iterateResolvers [srv1, srv2] (recording callsRef (\_ -> pure $ Left backendErr))
|
||||
case r of
|
||||
Left (ResolverTransport e) -> e `shouldBe` backendErr
|
||||
other -> expectationFailure $ "expected ResolverTransport, got " <> show other
|
||||
-- a backend failure on srv1 stops iteration (the name reached a capable
|
||||
-- relay); it must not be reported as the authoritative NameNotRegistered.
|
||||
readIORef callsRef `shouldReturn` [srv1]
|
||||
it "iterates on transport-level errors and uses the next server's success" $ do
|
||||
callsRef <- newIORef []
|
||||
r <- iterateResolvers [srv1, srv2] (recording callsRef stubTransportThenHit)
|
||||
r `shouldBe` Right sampleRecord
|
||||
-- both servers must be consulted, in order: first server was unreachable.
|
||||
readIORef callsRef `shouldReturn` [srv1, srv2]
|
||||
it "returns ResolverUnavailable when every server is unreachable (all transport)" $ do
|
||||
let r = runIdentity $ iterateResolvers [srv1, srv2] (\_ -> pure $ Left networkErr)
|
||||
r `shouldBe` Left ResolverUnavailable
|
||||
it "returns ResolverUnavailable on an empty server list" $ do
|
||||
let r = runIdentity $ iterateResolvers [] (\_ -> pure $ Right sampleRecord)
|
||||
r `shouldBe` Left ResolverUnavailable
|
||||
-- resolveErrorToChatError is the pure mapping used by connectPlanName's
|
||||
-- resolveAndDispatch: it converts the resolver outcome (when not a hit) to
|
||||
-- the user-visible ChatError. The success path (Right NameRecord) is dispatched
|
||||
-- via dispatchResolvedRecord (Task 6); we only exercise the failure mapping here.
|
||||
describe "resolveErrorToChatError" $ do
|
||||
it "maps NameNotRegistered to CESimplexNameNotFound (local-miss UX)" $
|
||||
case resolveErrorToChatError aliceNi NameNotRegistered of
|
||||
ChatError (CESimplexNameNotFound ni) -> ni `shouldBe` aliceNi
|
||||
other -> expectationFailure $ "expected CESimplexNameNotFound, got " <> show other
|
||||
it "maps ResolverUnavailable to CESimplexNameResolverUnavailable" $
|
||||
case resolveErrorToChatError aliceNi ResolverUnavailable of
|
||||
ChatError (CESimplexNameResolverUnavailable ni) -> ni `shouldBe` aliceNi
|
||||
other -> expectationFailure $ "expected CESimplexNameResolverUnavailable, got " <> show other
|
||||
it "wraps ResolverTransport via chatErrorAgent so the UI reuses agent-error rendering" $
|
||||
case resolveErrorToChatError aliceNi (ResolverTransport otherDefiniteErr) of
|
||||
ChatErrorAgent e _ _ -> e `shouldBe` otherDefiniteErr
|
||||
other -> expectationFailure $ "expected ChatErrorAgent, got " <> show other
|
||||
-- firstNameLink is the pure link-picker used by dispatchResolvedRecord:
|
||||
-- it selects nrSimplexContact for NTContact, nrSimplexChannel for NTPublicGroup.
|
||||
-- The text fields use the empty string as the "absent" sentinel; an empty
|
||||
-- link for the queried type collapses to CESimplexNameNotFound so the UX is
|
||||
-- identical to a local-store miss.
|
||||
-- An empty link for the queried type collapses to CESimplexNameNotFound so the
|
||||
-- UX is identical to a local-store miss.
|
||||
describe "firstNameLink" $ do
|
||||
it "NTContact path picks simplexContact" $
|
||||
case firstNameLink NTContact [channelLink] [contactLink] aliceNi of
|
||||
@@ -183,19 +102,6 @@ sampleShortLinkServer = case strDecode (T.encodeUtf8 sampleShortLinkServerText)
|
||||
Right (ACL SCMContact (CLShort l)) -> l
|
||||
other -> error $ "ResolveNameTests fixture failed to parse: " <> show other
|
||||
|
||||
-- | Wrap a resolver to record which servers it was called for.
|
||||
recording :: IORef [SMPServer] -> (SMPServer -> IO (Either AgentErrorType NameRecord)) -> SMPServer -> IO (Either AgentErrorType NameRecord)
|
||||
recording ref f srv = modifyIORef' ref (<> [srv]) >> f srv
|
||||
|
||||
stubAuthThenHit :: SMPServer -> IO (Either AgentErrorType NameRecord)
|
||||
stubAuthThenHit srv = pure $ M.findWithDefault (Right sampleRecord) srv $ M.fromList [(srv1, Left authErr)]
|
||||
|
||||
stubProhibitedThenHit :: SMPServer -> IO (Either AgentErrorType NameRecord)
|
||||
stubProhibitedThenHit srv = pure $ M.findWithDefault (Right sampleRecord) srv $ M.fromList [(srv1, Left prohibitedErr)]
|
||||
|
||||
stubTransportThenHit :: SMPServer -> IO (Either AgentErrorType NameRecord)
|
||||
stubTransportThenHit srv = pure $ M.findWithDefault (Right sampleRecord) srv $ M.fromList [(srv1, Left networkErr)]
|
||||
|
||||
aliceNi :: SimplexNameInfo
|
||||
aliceNi = SimplexNameInfo NTContact (SimplexNameDomain TLDSimplex "alice" [])
|
||||
|
||||
@@ -209,57 +115,3 @@ groupNi = SimplexNameInfo NTPublicGroup (SimplexNameDomain TLDSimplex "team" [])
|
||||
channelLink, contactLink :: Text
|
||||
channelLink = "simplex:/channel-team"
|
||||
contactLink = "simplex:/contact-alice"
|
||||
|
||||
srv1 :: SMPServer
|
||||
srv1 = SMPServer "smp1.example" "5223" (C.KeyHash "\1\2\3\4")
|
||||
|
||||
srv2 :: SMPServer
|
||||
srv2 = SMPServer "smp2.example" "5223" (C.KeyHash "\5\6\7\8")
|
||||
|
||||
sampleRecord :: NameRecord
|
||||
sampleRecord =
|
||||
NameRecord
|
||||
{ nrName = "alice",
|
||||
nrNickname = "",
|
||||
nrWebsite = "",
|
||||
nrLocation = "",
|
||||
nrSimplexContact = [],
|
||||
nrSimplexChannel = [],
|
||||
nrEth = Nothing,
|
||||
nrBtc = Nothing,
|
||||
nrXmr = Nothing,
|
||||
nrDot = Nothing,
|
||||
-- mkNameOwner enforces the 20-byte invariant; these strings are intentionally 20 ASCII bytes.
|
||||
nrOwner = either error id $ mkNameOwner "owner-bytes-1234567x",
|
||||
nrResolver = either error id $ mkNameOwner "resolver-bytes12345x"
|
||||
}
|
||||
|
||||
-- AUTH from a name-capable destination relay: surfaces as SMP host AUTH
|
||||
-- (see Simplex.Messaging.Agent.Client.protocolClientError).
|
||||
authErr :: AgentErrorType
|
||||
authErr = SMP "smp1.example" SMP.AUTH
|
||||
|
||||
-- A relay with no resolver configured (names role off) answers CMD PROHIBITED
|
||||
-- (Server.hs). A relay error is transparent over the proxy (SMP host ...); the
|
||||
-- PROXY-wrapped form here exercises a proxy-level rejection. Both -> skip.
|
||||
prohibitedErr :: AgentErrorType
|
||||
prohibitedErr = PROXY "proxy.example" "smp1.example" (ProxyProtocolError (SMP.CMD SMP.PROHIBITED))
|
||||
|
||||
-- BROKER NETWORK: the server is unreachable. This is the kind of failure
|
||||
-- that should cause iteration to fall through to the next configured server,
|
||||
-- because no information about the queried name has been disclosed.
|
||||
networkErr :: AgentErrorType
|
||||
networkErr = BROKER "smp1.example" (NETWORK (NEConnectError "simulated network failure"))
|
||||
|
||||
-- A definite, non-transport agent error: the server responded but in a way
|
||||
-- that doesn't match NAME / AUTH / CMD PROHIBITED. Should surface to the user
|
||||
-- as ResolverTransport without iterating, to avoid broadcasting the name.
|
||||
otherDefiniteErr :: AgentErrorType
|
||||
otherDefiniteErr = INTERNAL "simulated definite error"
|
||||
|
||||
-- A resolver-backed relay whose backing store failed (resolver 5xx, timeout,
|
||||
-- decode error) answers ERR INTERNAL (Server.hs), surfacing as SMP host INTERNAL.
|
||||
-- This is transient -- it must surface as ResolverTransport, NOT collapse to the
|
||||
-- authoritative NameNotRegistered the way the old ERR-AUTH-for-everything did.
|
||||
backendErr :: AgentErrorType
|
||||
backendErr = SMP "smp1.example" SMP.INTERNAL
|
||||
|
||||
Reference in New Issue
Block a user