diff --git a/bots/api/COMMANDS.md b/bots/api/COMMANDS.md index 506efc1661..fe0af33bd8 100644 --- a/bots/api/COMMANDS.md +++ b/bots/api/COMMANDS.md @@ -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" diff --git a/bots/api/EVENTS.md b/bots/api/EVENTS.md index 32d2d9aeda..947c60586a 100644 --- a/bots/api/EVENTS.md +++ b/bots/api/EVENTS.md @@ -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. diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 2451048137..9b64fddca7 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -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 diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index 42092bd7c3..22eeb45c03 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -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, ""), diff --git a/bots/src/API/Docs/Events.hs b/bots/src/API/Docs/Events.hs index 6a41e9ba66..f0c9352efd 100644 --- a/bots/src/API/Docs/Events.hs +++ b/bots/src/API/Docs/Events.hs @@ -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 ] ), diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index ddd127241b..ed5403d8e5 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -89,6 +89,7 @@ chatResponsesDocsData = ("CRSentConfirmation", "Confirmation sent to one-time invitation"), ("CRSentGroupInvitation", "Group invitation sent"), ("CRSentInvitation", "Invitation sent to contact address"), + ("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"), diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index 6934321241..bb6843f379 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -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 diff --git a/cabal.project b/cabal.project index 668fbc74f0..ed8213f7c3 100644 --- a/cabal.project +++ b/cabal.project @@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: df1aa24caa9f0546d8623da84d087139ee931f06 + tag: ce69adfdb2902e7005d71b05a0f41143d5632ec9 source-repository-package type: git diff --git a/packages/simplex-chat-client/types/typescript/src/commands.ts b/packages/simplex-chat-client/types/typescript/src/commands.ts index e25a2d8fc4..3c54de2c2d 100644 --- a/packages/simplex-chat-client/types/typescript/src/commands.ts +++ b/packages/simplex-chat-client/types/typescript/src/commands.ts @@ -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) diff --git a/packages/simplex-chat-client/types/typescript/src/events.ts b/packages/simplex-chat-client/types/typescript/src/events.ts index a3299acdf4..cc19305913 100644 --- a/packages/simplex-chat-client/types/typescript/src/events.ts +++ b/packages/simplex-chat-client/types/typescript/src/events.ts @@ -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 diff --git a/packages/simplex-chat-client/types/typescript/src/responses.ts b/packages/simplex-chat-client/types/typescript/src/responses.ts index 0fcf0e6eca..161ec65377 100644 --- a/packages/simplex-chat-client/types/typescript/src/responses.ts +++ b/packages/simplex-chat-client/types/typescript/src/responses.ts @@ -48,6 +48,7 @@ export type ChatResponse = | CR.SentConfirmation | CR.SentGroupInvitation | CR.SentInvitation + | CR.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 diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index e8b9fdf3c8..181b79f57c 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -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", diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py index a64752ad44..d8c957055c 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_commands.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_commands.py @@ -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 diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_events.py b/packages/simplex-chat-python/src/simplex_chat/types/_events.py index 581df22a60..7b7c724c92 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_events.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_events.py @@ -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]]], diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py index e85de02c78..48c094f7fd 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_responses.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_responses.py @@ -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"] diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 45110a3007..95a4eea2bc 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -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"] diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 42bf8e5e50..94acdcdd85 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -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"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index dd786f2029..407c8198dc 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -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: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 657d20cb3f..bb2dde9701 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -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) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 3cc03e3129..b3b0a76c4d 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -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 diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 8317ac81d1..ae75a493e2 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -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 diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 975b5adf2b..28ca07bddc 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -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' diff --git a/src/Simplex/Chat/Operators/Presets.hs b/src/Simplex/Chat/Operators/Presets.hs index 53f31e005d..3dd4add804 100644 --- a/src/Simplex/Chat/Operators/Presets.hs +++ b/src/Simplex/Chat/Operators/Presets.hs @@ -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. diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 886797f94a..449429b040 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -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 diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index ce5a36e93c..da00d578eb 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -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' diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 02e9454b7f..2681fa44bf 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -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 diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260612_smp_role_names.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260612_smp_role_names.hs new file mode 100644 index 0000000000..89e8f8c803 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260612_smp_role_names.hs @@ -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; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 0aa2dc40b0..fdabe855a8 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -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 ); diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 749f87b059..464e80c4cb 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -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 diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index f2a521eeff..f4b60dd9e8 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -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 diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260606_simplex_name_verified.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260606_simplex_name_verified.hs index 2287d540e7..f54738216c 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260606_simplex_name_verified.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260606_simplex_name_verified.hs @@ -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| diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260612_smp_role_names.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260612_smp_role_names.hs new file mode 100644 index 0000000000..0afea68839 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260612_smp_role_names.hs @@ -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; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 11e27bf8d6..19275adfe4 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -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, diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index cbfb80f741..ec0c85388e 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -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} = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index a1f470a872..da4af5e11c 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -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) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index a1d7d2887a..0a2505c150 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -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 $ diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 79345d7858..2a25c987a0 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -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} } diff --git a/tests/ResolveNameTests.hs b/tests/ResolveNameTests.hs index 090db51ce2..87f550bc19 100644 --- a/tests/ResolveNameTests.hs +++ b/tests/ResolveNameTests.hs @@ -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