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:
shum
2026-06-13 07:40:36 +00:00
parent fa75978a10
commit 69dee10bd7
38 changed files with 371 additions and 838 deletions
+7 -4
View File
@@ -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"
-60
View File
@@ -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
View File
@@ -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
+1 -3
View File
@@ -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, ""),
-4
View File
@@ -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
]
),
+1
View File
@@ -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"),
+3 -5
View File
@@ -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
View File
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: 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 -1
View File
@@ -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";
+2
View File
@@ -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:
+3 -45
View File
@@ -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)
+38 -149
View File
@@ -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
-9
View File
@@ -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
+9 -32
View File
@@ -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'
+2 -2
View File
@@ -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.
+17 -29
View File
@@ -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
+43 -62
View File
@@ -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
);
+14 -12
View File
@@ -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
+3 -1
View File
@@ -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,
+29 -34
View File
@@ -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} =
+2 -2
View File
@@ -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)
+1 -23
View File
@@ -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 $
+1
View File
@@ -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
View File
@@ -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