diff --git a/bots/src/API/Docs/Types.hs b/bots/src/API/Docs/Types.hs index ac5992770e..3adcaef783 100644 --- a/bots/src/API/Docs/Types.hs +++ b/bots/src/API/Docs/Types.hs @@ -298,7 +298,7 @@ chatTypesDocsData = (sti @GroupLink, STRecord, "", [], "", ""), (sti @GroupLinkOwner, STRecord, "", [], "", ""), (sti @GroupLinkPlan, STUnion, "GLP", [], "", ""), - (sti @GroupMember, STRecord, "", [], "", ""), + (removeField "memberBadgeProof" $ sti @GroupMember, STRecord, "", [], "", ""), (sti @GroupMemberAdmission, STRecord, "", [], "", ""), (sti @GroupMemberCategory, STEnum' (dropPfxSfx "GC" "Member"), "", [], "", ""), (sti @GroupMemberRef, STRecord, "", [], "", ""), diff --git a/cabal.project b/cabal.project index f3edb1da25..d15e79a9aa 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: 25b2734280f1c969f12d349c791a9943cca01168 + tag: bf391d60eca9813ceb2f7ef77d104960a463e46e source-repository-package type: git diff --git a/docs/protocol/channels-protocol.md b/docs/protocol/channels-protocol.md index 6a232ea2ff..84753aa590 100644 --- a/docs/protocol/channels-protocol.md +++ b/docs/protocol/channels-protocol.md @@ -44,11 +44,11 @@ Creating a channel involves generating cryptographic material, creating the chan When a relay receives an invitation to serve a channel, it validates the channel and creates its own relay link. This flow is currently part of channel creation; adding relays to an existing channel is planned but not yet implemented. -1. Owner sends `x.grp.relay.inv` to the relay's contact address. This message includes the relay's member ID and role, the owner's profile, and the channel's short link. +1. Owner sends `x.grp.relay.inv` to the relay's contact address. This message includes the relay's member ID and role, the owner's profile, the channel's short link, and the channel's public group ID. -2. Relay receives the invitation and creates a relay request record. A relay request worker processes it asynchronously. +2. Relay receives the invitation and creates a relay request record. The owner's badge is verified under the channel binding of the public group ID from the invitation. A relay request worker processes it asynchronously. -3. The worker retrieves the channel's link data from the SMP server, extracts and validates the channel profile and owner authorization. +3. The worker retrieves the channel's link data from the SMP server, extracts and validates the channel profile and owner authorization. The request fails when the link's entity ID differs from the public group ID in the invitation. 4. The relay creates its own contact address link (the relay link) with the channel's entity ID in the immutable fixed data. diff --git a/plans/2026-09-04-badge-binding-and-file-limits.md b/plans/2026-09-04-badge-binding-and-file-limits.md index d5ba67385b..1224893ae2 100644 --- a/plans/2026-09-04-badge-binding-and-file-limits.md +++ b/plans/2026-09-04-badge-binding-and-file-limits.md @@ -18,7 +18,7 @@ The changes: 5. Forwarding a file above the forwarder's limit is refused with an alert before the forwarding sheet opens, and again, for the chosen destination, before anything is uploaded. 6. A received file keeps its two proofs, so a file re-sent to a new member as part of history keeps them; the sender's own files get fresh proofs from the credential. -Two new columns on `files`, and a new table `file_badge_proofs` holding the invitation proof and the description proof of a file, kept for history. A new column on `connections`, the request header of a prepared connection. In simplexmq: the hash of the fields shared by all descriptions of one upload, the verification codes of a connection, links and invitations before link data is signed, and the request code of an invitation. +Two new columns on `files`, and a new table `file_badge_proofs` holding the invitation proof and the description proof of a file, kept for history. The same table holds the proof of a group member, forwarded in introductions. A new column on `connections`, the request header of a prepared connection. The relay invitation includes the channel's public group id. In simplexmq: the hash of the fields shared by all descriptions of one upload, the verification codes of a connection, links and invitations before link data is signed, and the request code of an invitation. ## Terms @@ -46,7 +46,7 @@ Two new columns on `files`, and a new table `file_badge_proofs` holding the invi **Presentation header.** A byte string that is an input to proof generation and to proof verification. A proof verifies only with the header it was generated with. Type `ProofPresHeader` in `Badges.hs`. `PHTest` is a random nonce. -**Chat binding.** The byte string that identifies the sender in one conversation, produced by `encodeChatBinding` (`Protocol.hs:444`). Message signatures and shared contact cards are computed over it. For a direct chat it is `encodeChatBinding CBDirect codeAD`, where `codeAD` is the hash of the connection's ratchet data, which both sides obtain with `getConnectionVerifyCodes`. For a p2p group it is `encodeChatBinding CBGroup (smpEncode (memberId, memberKey))`. For a channel it is `encodeChatBinding CBGroup (smpEncode (publicGroupId, memberId))`. For a link it is `encodeChatBinding CBLink linkKey`. `groupBindingData` (`Internal.hs:2322`) computes the inner part for groups. +**Chat binding.** The byte string that identifies the sender in one conversation, produced by `encodeChatBinding` (`Protocol.hs:444`). Message signatures and shared contact cards are computed over it. For a direct chat it is `encodeChatBinding CBDirect codeAD`, where `codeAD` is the hash of the connection's ratchet data, which both sides obtain with `getConnectionVerifyCodes`. For a p2p group it is `encodeChatBinding CBGroup (smpEncode (memberId, memberKey))`. For a channel it is `encodeChatBinding CBGroup (smpEncode (publicGroupId, memberId))`. `groupBindingData` (`Internal.hs:2322`) computes the inner part for groups. **Member key.** The Ed25519 key a member holds for one group. It is created when first needed — at group creation on this branch, or by `createUserMemberKey` before the first signed message — and the public key is sent in introductions and in `XInfo`. @@ -65,11 +65,13 @@ data ProofPresHeader | PHFileInv ByteString String Integer | PHFileDescr ByteString String Integer ByteString (Maybe UTCTime) | PHRequest ByteString + | PHLink ByteString | PHUnknown Char ByteString ``` - `PHChat` holds the chat binding. - `PHRequest` holds the request code (section 14.1). +- `PHLink` holds the link key. - `PHFileInv` holds the chat binding and the file size from the invitation. - `PHFileDescr` holds the same two values, then the shared description hash (section 8) and the file expiration. @@ -280,7 +282,7 @@ Rules: | Request to an address with ratchet keys | address owner | `PHChat (encodeChatBinding CBDirect codeAD)` | | Request to an address without ratchet keys | address owner, group host | `PHRequest code` — the joining party's keys, the address queue | | One-time invitation link data | joining party | `PHRequest code` — the inviter's keys, the invitation queue | -| Address link data, shared address card | anyone with the link | `PHChat (encodeChatBinding CBLink linkKey)` | +| Address link data, shared address card | anyone with the link | `PHLink linkKey` | | P2p group | members | `PHChat (encodeChatBinding CBGroup (smpEncode (memberId, memberKey)))` | | Channel | subscribers | `PHChat (encodeChatBinding CBGroup (smpEncode (publicGroupId, memberId)))` | @@ -336,45 +338,52 @@ data ContactRequestBinding = CRBRatchet ConnVerifyCodes | CRBRequest ByteString Files: `Agent.hs`, `Agent/Protocol.hs`, `tests/AgentTests/FunctionalAPITests.hs`. +Committed in simplexmq `25b27342`. The chat pins it in `cabal.project` and `scripts/nix/sha256map.nix`. + Every link, and every invitation, is available to the chat before its link data is signed; each creation makes one network request. **New links.** The link, and for an invitation the invitation, are returned by `prepareConnectionLink` before the user data is signed, in both connection modes: ```haskell -prepareConnectionLink :: ConnectionModeI c => AgentClient -> UserId -> SConnectionMode c -> C.KeyPairEd25519 -> Maybe ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AE (CreatedConnLink c, PreparedLinkParams) +prepareConnectionLink :: AgentClient -> UserId -> SConnectionMode c -> C.KeyPairEd25519 -> Maybe ByteString -> Bool -> Maybe CRClientData -> CR.InitialKeys -> UseRatchetKeys -> Maybe SMPServerWithAuth -> AE (CreatedConnLink c, PreparedLinkParams c) -createConnectionForLink :: ConnectionModeI c => AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink c -> PreparedLinkParams -> UserConnLinkData c -> SubscriptionMode -> AE (ConnId, CreatedConnLink c) +createConnectionForLink :: AgentClient -> NetworkRequestMode -> UserId -> Bool -> CreatedConnLink c -> PreparedLinkParams c -> UserConnLinkData c -> SubscriptionMode -> AE (ConnId, CreatedConnLink c) ``` - The link entity id is optional; `Nothing` for an invitation. -- A new field in `PreparedLinkParams`: +- `PreparedLinkParams` is indexed by the connection mode. The ratchet keys are one field, `plpRatchetKeys :: PreparedRatchetKeys m`, in place of `plpAddressKeys`: ```haskell - plpInvitationKeys :: Maybe (RcvE2EPrivRatchetParams 'C.X448) + data PreparedRatchetKeys (m :: ConnectionMode) where + PRKInvitation :: RcvE2EPrivRatchetParams 'C.X448 -> PreparedRatchetKeys 'CMInvitation + PRKContact :: Maybe (RatchetKeyId, RcvE2EPrivRatchetParams 'C.X448) -> PreparedRatchetKeys 'CMContact ``` -- Contact mode is unchanged; the link given to `createConnectionForLink` is returned. +- Contact mode: the link given to `createConnectionForLink` is returned. - Invitation mode, prepare: - x3dh keys from `CR.generateRcvE2EParams`, PQ support from `CR.initialPQEncryption True pqInitKeys` - `connReq = CRInvitationUri crData e2eRcvParams`, queue mode `QMMessaging` - the returned link is `CCLink connReq Nothing`; the key is `plpLinkKey` - `useDR` is ignored - Invitation mode, create: - - `plpInvitationKeys` are stored with `createRatchetX3dhKeys` - - link data: `SL.encodeSignUserData SCMInvitation`, encrypted with `SL.invShortLinkKdf plpLinkKey` + - the `PRKInvitation` keys are stored with `createRatchetX3dhKeys` + - link data: `SL.encodeSignUserData SCMInvitation`, encrypted by `encryptInvLinkData` with `SL.invShortLinkKdf plpLinkKey`; `newRcvConnSrv` uses the same function - queue request: `CQRMessaging (Just CQRData {linkKey, privSigKey, srvReq = (sndId, srvData)})` - - the returned link: from `connReqWithShortLink`, moved from `newRcvConnSrv` to top level — `CSLInvitation` with the link id from the server, PQ keys removed from the full link for `IKPQOn` +- Both modes create the queue with the local `createLinkQueue`: + - `createRcvQueue` + - the sender id check of `createConnectionForLink'` on master, error `sender ID mismatch` + - the returned link from `connReqWithShortLink`, moved from `newRcvConnSrv` to top level with its body unchanged — `CSLInvitation` with the link id from the server, PQ keys removed from the full link for `IKPQOn` - Tests: a connection via an invitation made by prepare and create; its link data is read by the joining party; `plpLinkKey` equals the key in the returned `CSLInvitation`. The six existing test calls are updated for the mode and the pair. **Existing connections.** The link of a connection is returned without a network call: ```haskell -prepareConnShortLink :: AgentClient -> ConnId -> Maybe CRClientData -> Maybe CR.InitialKeys -> AE (ConnShortLink 'CMContact) +prepareConnShortLink :: AgentClient -> ConnId -> Maybe CRClientData -> AE (ConnShortLink 'CMContact) ``` - A stored link is returned as is. -- Otherwise the address ratchet keys are chosen from `InitialKeys` (`addressRatchetKeys`), and the credentials are created and stored (`newContactLinkCreds`): the signing key pair is generated, the fixed data is built, signed and encrypted (`SL.encryptFixedData`), and `ShortLinkCreds` are stored. -- `setConnShortLink` uses the same two functions for a connection without stored credentials, and then encrypts and uploads the user data with one `LSET`. +- Otherwise the credentials are created and stored by `newContactLinkCreds :: AgentClient -> RcvQueue -> Maybe CRClientData -> AM ShortLinkCreds`: the signing key pair is generated, the fixed data is built from the connection request without ratchet keys, signed and encrypted (`SL.encryptFixedData`), and `ShortLinkCreds` are stored. +- `setConnShortLink` uses `newContactLinkCreds` for a connection without stored credentials, and then encrypts and uploads the user data with one `LSET`. - Tests: for an address without a short link, the link is the same from two `prepareConnShortLink` calls and from `setConnShortLink`; a requester connects via it. **Invitation code.** Exported: @@ -389,12 +398,12 @@ invitationRequestCode :: ConnectionRequestUri 'CMInvitation -> ByteString ### 14.3 Chat -Todo, in implementation order. At every send, a `Nothing` from a header helper is replaced by `unboundPresHeader`. +Todo, in implementation order. At every direct send, a `Nothing` from `connPresHeader` or `connsPresHeaders`, and a retry without a stored request header, get `unboundPresHeader`. At a send into a group, a `Nothing` from `groupPresHeader` presents no badge. -**1. Headers** — `Badges.hs`, `Protocol.hs` +**1. Headers** — `Badges.hs` - `PHRequest ByteString` in `ProofPresHeader`, tag `'R'`. -- `CBLink` in `ChatBinding`, tag `'L'`; payload: the link key bytes. +- `PHLink ByteString` in `ProofPresHeader`, tag `'L'`; payload: the link key bytes. - `acceptedProof :: Maybe ProofPresHeader -> BadgeProof -> Bool`: true for `PHTest`, and for a header equal to `strEncode` of the expected one. `unboundProof` and `boundProof` are replaced by it. - `ToField` and `FromField` for `ProofPresHeader`: the `strEncode` bytes as a blob. @@ -436,8 +445,8 @@ With `Nothing`, no badge is presented. Header helpers: - `groupPresHeader :: GroupInfo -> Maybe ProofPresHeader` — `PHChat <$> sndGroupChatBinding gInfo False` - `directPresHeader :: ContactRequestBinding -> ProofPresHeader` — `PHChat (encodeChatBinding CBDirect codeAD)` for `CRBRatchet`, `PHRequest code` for `CRBRequest` -- `linkPresHeader :: LinkKey -> ProofPresHeader` — `PHChat (encodeChatBinding CBLink key)` -- a one-time invitation's header: `PHRequest (invitationRequestCode connReq)` +- `linkPresHeader :: LinkKey -> ProofPresHeader` — `PHLink key` +- `invitationPresHeader :: ConnReqInvitation -> ProofPresHeader` — `PHRequest (invitationRequestCode connReq)` - `unboundPresHeader :: CM ProofPresHeader` — `PHTest` of 16 random bytes - `connPresHeader :: Connection -> CM (Maybe ProofPresHeader)` — `directPresHeader . CRBRatchet` of `getConnectionVerifyCodes`; `Nothing` on error - `connsPresHeaders :: [Connection] -> CM (Map ConnId ProofPresHeader)` — the same from `getConnectionsVerifyCodes` @@ -487,13 +496,13 @@ ALTER TABLE connections ADD COLUMN pres_header BLOB; - Host `INFO` with `XInfo` (`Subscriber.hs:859-864`): after `storeMemberKey`, the profile is stored by `processMemberProfileUpdate` with `PHChat <$> signedMemberBinding`, under the member's key. - The profile is also stored by `processMemberProfileUpdate` when the new proof is accepted and its header differs from the stored proof's header. - Introductions: - - In `memberInfo` (`Internal.hs:1330`) the stored proof is included when `acceptedProof (PHChat <$> memberChatBinding g memberId memberPubKey)` holds, and omitted otherwise. + - In `memberInfo` (`Internal.hs:1331`) the stored proof (item 11) is included when `acceptedProof (PHChat <$> memberChatBinding g memberId memberPubKey)` holds, and omitted otherwise. - In `xGrpMemNew`, `xGrpMemIntro` and `xGrpMemFwd` (`Subscriber.hs:3197, 3279, 3341`): `PHChat <$> memberChatBinding gInfo memId key`, with `key` from the `MemberInfo`. - Invitation via contact: - `profile :: Maybe Profile` in `XGrpAcpt`, the optional JSON field `profile`. - The invitee (`Commands.hs:2845`, `Subscriber.hs:2697`) includes its group profile, with `groupPresHeader`, when the maximum of the contact connection's `peerChatVRange` is at least `relayWebCapVersion`; the message is encoded with `encodeSignedConnInfo` when a signing is returned by `groupMsgSigning`. - `XGrpAcpt` with a badge in its profile is signed by `groupMsgSigning` (`Internal.hs:2316-2319`). - - The host (`Subscriber.hs:786`) stores the key, then the profile with `PHChat <$> signedMemberBinding`. + - The host (`Subscriber.hs:786`) stores the key, then the profile with `PHChat <$> signedMemberBinding`. For an invitee whose contact is active, the profile row is kept by `canUpdateProfile` and the proof is stored on the membership (item 11). - The host replies with `XGrpMemInfo` and its group profile in place of `XOk` (`Subscriber.hs:791`); the badge is presented when the invitee's version is at least `relayWebCapVersion`, as at `:840-846`. **10. Link data** @@ -509,13 +518,83 @@ ALTER TABLE connections ADD COLUMN pres_header BLOB; - Shared address card: - `APIShareMyAddress` (`Commands.hs:1243-1255`): the badge is presented with `linkPresHeader` of `connLink`. - Receipts (`Subscriber.hs:1910-1916`, `:2217-2221`): the proof in `MCLContact.profile` is kept when accepted under `linkPresHeader` of `connLink` and verified, and removed otherwise. - - The badge from `profile.badge` is shown in `CIChatLinkHeader`, on iOS and in the multiplatform app. + - The badge from `profile.badge` is shown in `CIChatLinkHeader`, on iOS and in the multiplatform app. Its status is computed by the app from the expiry: active until seven days past it, expired until 38 days past it, hidden after. -**11. Tests** — `ChatTests/Profiles.hs` +**11. Member proofs** — `Badges.hs`, `Types.hs`, `Types/Preferences.hs`, `Store/Shared.hs`, `Store/Groups.hs`, `Store/Connections.hs`, `Internal.hs`, `Subscriber.hs` + +A member's accepted proof is stored on the membership, in `file_badge_proofs`, and forwarded in introductions. The profile row keeps its badge for display. + +Migration `M20260925_member_badge_proofs`, SQLite: + +```sql +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'file_id INTEGER NOT NULL REFERENCES files', 'file_id INTEGER REFERENCES files') +WHERE name = 'file_badge_proofs' AND type = 'table'; + +PRAGMA writable_schema=RESET; + +ALTER TABLE file_badge_proofs ADD COLUMN group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE; + +CREATE UNIQUE INDEX idx_file_badge_proofs_group_member_id ON file_badge_proofs(group_member_id); +``` + +Postgres: + +```sql +ALTER TABLE file_badge_proofs ALTER COLUMN file_id DROP NOT NULL; + +ALTER TABLE file_badge_proofs ADD COLUMN group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE; + +CREATE UNIQUE INDEX idx_file_badge_proofs_group_member_id ON file_badge_proofs(group_member_id); +``` + +The down migration deletes member rows before `file_id` is `NOT NULL` again. Both schema dumps and `chat_query_plans.txt` are updated. + +- A member row has `group_member_id` set, `file_id` NULL, and `proof_kind` `member`: `BPKMember` in `BadgeProofKind`. `getFileBadgeProofs` ignores it. +- `MaybeBadgeProofRow` and `maybeRowToBadgeProof` in `Badges.hs`: the six proof columns of a `LEFT JOIN`. +- `PrefsJSON` is generalised: + + ```haskell + newtype NoJSON a = NoJSON {unNoJSON :: Maybe a} + + type PrefsJSON = NoJSON Object + ``` + + `ToJSON` omits the field; `FromJSON` gives `NoJSON Nothing`. The construction sites use `NoJSON`. +- `GroupMember` gains `memberBadgeProof :: NoJSON BadgeProof`. The bot API docs remove it: `removeField "memberBadgeProof" $ sti @GroupMember`. +- Reads: + - `groupMemberQuery` selects the proof columns after the connection columns, with `LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id`; `toContactMember` sets the field. + - The member read of `getConnectionEntity` (`Connections.hs:145-194`) selects them the same way; `toGroupAndMember` sets the field. + - `toGroupMember` sets `NoJSON Nothing`, for the membership, chat item members and quoted members. +- Writes, for a received profile: + - a badge accepted by `acceptedProof` under the expected header is upserted; + - a profile without a badge deletes the row; + - a badge not accepted leaves the row as it is. +- `setMemberBadgeProof :: DB.Connection -> GroupMember -> Maybe ProofPresHeader -> Profile -> IO GroupMember` applies these rules and returns the member with the field set. Used by `updateMemberProfile` and `updateContactMemberProfile`, and by `processMemberProfileUpdate` when `canUpdateProfile` is false. +- `processMemberProfileUpdate` passes the received profile to the store functions. The profile with the stored proof in place of a rejected one is used for the comparisons and the chat item only. +- At creation the accepted proof is inserted: `createNewMember_` (for `createNewGroupMember` and `createIntroReMember`), `createJoiningMember`, and the owner in `createRelayRequestGroup`. +- `memberInfo` includes `memberBadgeProof`, or the profile's proof when it is absent, filtered by `acceptedProof` as before. + +**12. The channel owner at the relay** — `Types.hs`, `Commands.hs`, `Subscriber.hs`, `Internal.hs`, `Store/Groups.hs`, `Store/Shared.hs` + +- `GroupRelayInvitation` gains `publicGroupId :: Maybe B64UrlByteString`, the optional JSON field `publicGroupId`. The owner sets it in `addRelays` from `publicGroup' gInfo`. +- `relayInvPresHeader :: GroupRelayInvitation -> Maybe ProofPresHeader` — `PHChat (encodeChatBinding CBGroup (smpEncode (publicGroupId, memberId)))` with the owner's member id; `Nothing` without the claim. +- `createRelayRequestGroup` takes it as the expected header, at `xGrpRelayInv` and `rejectRelayInvitationAsync`. The owner's profile and membership proof are stored under it. +- The claim is written to the placeholder's `group_profiles.public_group_id`. `group_type` and `group_link` stay NULL, so `publicGroup` is `Nothing` and `mkGroupKeys` returns `GKRelayRequest`. +- `GroupKeysRow` gains the profile's `public_group_id`, set by `toGroupInfo` from the same row. `GKRelayRequest` gains `publicGroupId :: Maybe B64UrlByteString`, set by `mkGroupKeys`. +- `getLinkDataCreateRelayLink` fails the request when the claim differs from the link's entity id. The comparison is skipped for a request without a claim. +- `getGroupViaPublicGroupId` matches rows with a group link only. +- `updateRelayGroupKeys` writes the link's id over the claim. + +**13. Tests** — `ChatTests/Profiles.hs`, `ChatTests/ChatRelays.hs` - Direct: the badge is shown after a request to an address with and without ratchet keys, after accepting, after joining a one-time link, after a retried join, and after a profile update. - A proof bound to another chat is ignored, and the stored badge is kept. - P2p group: the joiner's badge is shown at the host at the request and after `INFO`; an introduced member's badge is shown at introduction; the badge is shown on both sides of an invitation via contact. +- Invitation via contact: the invitee's badge, stored on the membership at the host, is shown at a member introduced after the invitee joins. +- Channel: the owner's badge is shown at a subscriber, forwarded by the relay (`testChannelMemberBadges`). - Link data: the badge is shown from an invitation link, an address, an address that gets its first short link, and a shared address card. `PHTest` is still sent by released clients and, through `unboundPresHeader`, on a retry of a connection prepared before this change. diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index eccaae5928..4989df0bdf 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."25b2734280f1c969f12d349c791a9943cca01168" = "023kwnpmrlgzsmp0y2avwgp5v1683lyir6mxm2mcc2rsfgbq049m"; + "https://github.com/simplex-chat/simplexmq.git"."bf391d60eca9813ceb2f7ef77d104960a463e46e" = "0fgq7xnjf7w03fjj49mal97g9mija5572mggb817ad0i2ds42z9i"; "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 690ccbaa20..3eb1c04d3b 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -169,6 +169,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json Simplex.Chat.Store.Postgres.Migrations.M20260924_conn_pres_header + Simplex.Chat.Store.Postgres.Migrations.M20260925_member_badge_proofs else exposed-modules: Simplex.Chat.Archive @@ -347,6 +348,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json Simplex.Chat.Store.SQLite.Migrations.M20260924_conn_pres_header + Simplex.Chat.Store.SQLite.Migrations.M20260925_member_badge_proofs other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Badges.hs b/src/Simplex/Chat/Badges.hs index 203d4d37b6..8a1b077c2a 100644 --- a/src/Simplex/Chat/Badges.hs +++ b/src/Simplex/Chat/Badges.hs @@ -54,6 +54,8 @@ module Simplex.Chat.Badges BadgeProofRow, badgeProofToRow, rowToBadgeProof, + MaybeBadgeProofRow, + maybeRowToBadgeProof, badgeToRow, localBadgeToRow, rowToBadge, @@ -141,16 +143,18 @@ instance TextEncoding BadgeStatus where -- Badge proof kind - a file has at most one proof of each kind -data BadgeProofKind = BPKInvitation | BPKDescription +data BadgeProofKind = BPKInvitation | BPKDescription | BPKMember deriving (Eq, Show) instance TextEncoding BadgeProofKind where textEncode = \case BPKInvitation -> "inv" BPKDescription -> "descr" + BPKMember -> "member" textDecode = \case "inv" -> Just BPKInvitation "descr" -> Just BPKDescription + "member" -> Just BPKMember _ -> Nothing -- Disclosed badge content (BBS messages 1, 2, 3) @@ -458,6 +462,12 @@ rowToBadgeProof (Binary p, Binary ph, idx, type_, badgeExpiry, badgeExtra) = do badgeType <- textDecode type_ pure $ BadgeProof idx (BBSPresHeader ph) (BBSProof p) BadgeInfo {badgeType, badgeExpiry, badgeExtra} +type MaybeBadgeProofRow = (Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe Int, Maybe Text, Maybe UTCTime, Maybe Text) + +maybeRowToBadgeProof :: MaybeBadgeProofRow -> Maybe BadgeProof +maybeRowToBadgeProof (p_, ph_, idx_, type_, expiry_, extra_) = + rowToBadgeProof =<< (,,,,,) <$> p_ <*> ph_ <*> idx_ <*> type_ <*> expiry_ <*> extra_ + -- (proof, pres_header, expiry, type, verified, extra, master_key, signature, key_idx) - binary columns wrapped in Binary (BLOB/bytea) type BadgeRow = (Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe UTCTime, Maybe Text, Maybe BoolInt, Maybe Text, Maybe (Binary ByteString), Maybe (Binary ByteString), Maybe Int) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index ffed89cd3d..a880502900 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -4341,7 +4341,8 @@ processChatCommand cxt nm = \case fromMember = MemberIdRole userMemberId userRole, fromMemberProfile = membershipProfile, relayMemberId, - groupLink = groupSLink + groupLink = groupSLink, + publicGroupId = (\PublicGroupProfile {publicGroupId = gId} -> gId) <$> publicGroup' gInfo } dm <- encodeConnInfo $ XGrpRelayInv relayInv sqSecured <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm PQSupportOff subMode diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 97fa787863..b1ef8842b0 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1184,7 +1184,7 @@ rejectRelayInvitationAsync -> CM () rejectRelayInvitationAsync user uclId cxt groupRelayInv invId reqChatVRange initialDelay reason = do (_gInfo, ownerMember) <- withStore $ \db -> - createRelayRequestGroup db cxt user groupRelayInv invId reqChatVRange initialDelay GSMemInvited RSRejected + createRelayRequestGroup db cxt user groupRelayInv (relayInvPresHeader groupRelayInv) invId reqChatVRange initialDelay GSMemInvited RSRejected let GroupMember {groupMemberId} = ownerMember msg = XGrpRelayReject reason subMode <- chatReadVar subscriptionMode @@ -1329,12 +1329,12 @@ userProfileInGroup' User {profile = p} mg incognitoProfile = in maybe p' (\g -> redactedMemberProfile g (membership g) p') mg memberInfo :: GroupInfo -> GroupMember -> MemberInfo -memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, activeConn} = +memberInfo g m@GroupMember {memberId, memberRole, memberProfile, memberPubKey, activeConn, memberBadgeProof} = MemberInfo { memberId, memberRole, v = ChatVersionRange . peerChatVRange <$> activeConn, - profile = (p :: Profile) {badge = mfilter (acceptedProof $ PHChat <$> memberChatBinding g memberId memberPubKey) badge}, + profile = (p :: Profile) {badge = mfilter (acceptedProof $ PHChat <$> memberChatBinding g memberId memberPubKey) (unNoJSON memberBadgeProof <|> badge)}, memberKey = MemberKey <$> memberPubKey } where @@ -2259,6 +2259,10 @@ linkPresHeader (LinkKey key) = PHLink key invitationPresHeader :: ConnReqInvitation -> ProofPresHeader invitationPresHeader = PHRequest . invitationRequestCode +relayInvPresHeader :: GroupRelayInvitation -> Maybe ProofPresHeader +relayInvPresHeader GroupRelayInvitation {fromMember = MemberIdRole {memberId}, publicGroupId} = + (\gId -> PHChat $ encodeChatBinding CBGroup $ smpEncode (gId, memberId)) <$> publicGroupId + unboundPresHeader :: CM ProofPresHeader unboundPresHeader = PHTest <$> drgRandomBytes 16 diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 60cb3b992d..8080629172 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -1658,7 +1658,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag then rejectRelayInvitationAsync user uclId cxt groupRelayInv invId chatVRange initialDelay RRRRejoinRejected else do (_gInfo, _ownerMember) <- withStore $ \db -> - createRelayRequestGroup db cxt user groupRelayInv invId chatVRange initialDelay GSMemAccepted RSInvited + createRelayRequestGroup db cxt user groupRelayInv (relayInvPresHeader groupRelayInv) invId chatVRange initialDelay GSMemAccepted RSInvited lift $ void $ getRelayRequestWorker True xGrpRelayTest :: InvitationId -> VersionRangeChat -> ByteString -> CM () xGrpRelayTest invId chatVRange challenge @@ -2934,7 +2934,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag when contentChanged $ updateBusinessChatProfile gInfo case memberContactId of Nothing -> do - m' <- withStore $ \db -> updateMemberProfile db cxt user m presHeader_ p'' + m' <- withStore $ \db -> updateMemberProfile db cxt user m presHeader_ (redactedMemberProfile gInfo m rcvProfile) unless (muteEventInChannel gInfo m') $ do when contentChanged $ forM_ msgTs_ $ createProfileUpdatedItem m' toView $ CEvtGroupMemberUpdated user gInfo m m' @@ -2943,13 +2943,13 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag mCt <- withStore $ \db -> getContact db cxt user mContactId if canUpdateProfile mCt then do - (m', ct') <- withStore $ \db -> updateContactMemberProfile db cxt user m mCt presHeader_ p' + (m', ct') <- withStore $ \db -> updateContactMemberProfile db cxt user m mCt presHeader_ rcvProfile unless (muteEventInChannel gInfo m') $ do when contentChanged $ forM_ msgTs_ $ createProfileUpdatedItem m' toView $ CEvtGroupMemberUpdated user gInfo m m' toView $ CEvtContactUpdated user mCt ct' pure m' - else pure m + else withStore' $ \db -> setMemberBadgeProof db m presHeader_ rcvProfile where canUpdateProfile ct | not (contactActive ct) = True @@ -4563,8 +4563,8 @@ runRelayRequestWorker a Worker {doWork} = do where getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfoKeys -> CM (GroupInfo, ShortLinkContact) getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} (GIK gInfo gks) = do - memberPrivKey' <- case gks of - GKRelayRequest {memberPrivKey} -> pure memberPrivKey + (memberPrivKey', claimedGroupId_) <- case gks of + GKRelayRequest {memberPrivKey, publicGroupId} -> pure (memberPrivKey, publicGroupId) _ -> throwChatError $ CEException "getLinkDataCreateRelayLink: group is not a relay request" (FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners}), _) <- getShortLinkConnReq' NRMBackground user reqGroupLink liftIO (decodeLinkUserData cData) >>= \case @@ -4572,8 +4572,8 @@ runRelayRequestWorker a Worker {doWork} = do Just GroupShortLinkData {groupProfile = gp@GroupProfile {publicGroup}} -> do pg <- case (linkEntityId, publicGroup) of (Just entityId, Just pg@PublicGroupProfile {publicGroupId}) - | B64UrlByteString entityId == publicGroupId -> pure pg - _ -> throwChatError $ CEException "getLinkDataCreateRelayLink: linkEntityId does not match profile publicGroupId" + | B64UrlByteString entityId == publicGroupId && all (== publicGroupId) claimedGroupId_ -> pure pg + _ -> throwChatError $ CEException "getLinkDataCreateRelayLink: linkEntityId does not match publicGroupId of profile or invitation" validateGroupProfile gp sLnk <- createRelayLink gInfo (C.publicKey memberPrivKey', memberPrivKey') gInfo' <- withStore $ \db -> do diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 0867118cb6..e47f043130 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -31,7 +31,7 @@ import Data.Bitraversable (bitraverse) import Data.Int (Int64) import Data.Maybe (fromMaybe) import Data.Time.Clock (UTCTime, getCurrentTime) -import Simplex.Chat.Badges (rowToBadge) +import Simplex.Chat.Badges (MaybeBadgeProofRow, maybeRowToBadgeProof, rowToBadge) import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Groups @@ -175,23 +175,25 @@ getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = d m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?) |] (groupMemberId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) liftIO $ bitraverse (\(g, keysData) -> (,keysData) <$> addGroupChatTags db g) pure gm - toGroupAndMember :: UTCTime -> Connection -> GroupInfoRow :. GroupMemberRow -> ((GroupInfo, GroupKeysRow), GroupMember) - toGroupAndMember currentTs c (groupInfoRow :. memberRow) = + toGroupAndMember :: UTCTime -> Connection -> GroupInfoRow :. GroupMemberRow :. MaybeBadgeProofRow -> ((GroupInfo, GroupKeysRow), GroupMember) + toGroupAndMember currentTs c (groupInfoRow :. memberRow :. proofRow) = let groupInfo = toGroupInfo currentTs cxt userContactId [] groupInfoRow member = toGroupMember currentTs userContactId memberRow - in (groupInfo, (member :: GroupMember) {activeConn = Just c}) + in (groupInfo, (member :: GroupMember) {activeConn = Just c, memberBadgeProof = NoJSON $ maybeRowToBadgeProof proofRow}) getUserContact_ :: Int64 -> ExceptT StoreError IO UserContact getUserContact_ userContactLinkId = ExceptT $ do userContact_ diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 1ba7c98fce..cbe1646caa 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -683,6 +683,7 @@ getFileBadgeProofs db fileId = foldl' addProof (Nothing, Nothing) <$> DB.query d addProof (inv_, descr_) (Only kind :. row) = case kind of BPKInvitation -> (rowToBadgeProof row, descr_) BPKDescription -> (inv_, rowToBadgeProof row) + BPKMember -> (inv_, descr_) toRcvFileDescr :: (Int64, Text, Int, BoolInt) -> RcvFileDescr toRcvFileDescr (fileDescrId, fileDescrText, fileDescrPartNo, BI fileDescrComplete) = diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index d7465a8aac..ba4afe581a 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -193,6 +193,7 @@ module Simplex.Chat.Store.Groups resetMemberContactFields, updateMemberProfile, updateContactMemberProfile, + setMemberBadgeProof, getXGrpLinkMemReceived, setXGrpLinkMemReceived, createNewUnknownGroupMember, @@ -230,7 +231,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime) import Data.Text.Encoding (encodeUtf8) -import Simplex.Chat.Badges (BadgeRow, ProofPresHeader, badgeToRow) +import Simplex.Chat.Badges (BadgeProof, BadgeProofKind (..), BadgeRow, ProofPresHeader, acceptedProof, badgeProofToRow, badgeToRow) import Simplex.Chat.Names (SimplexDomainClaim (..)) import Simplex.Chat.Messages import Simplex.Chat.Operators @@ -597,7 +598,8 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe supportChat = Nothing, memberPubKey, relayLink = Nothing, - memberVerifiedCode = Nothing + memberVerifiedCode = Nothing, + memberBadgeProof = NoJSON Nothing } where memberChatVRange@(VersionRange minV maxV) = vr @@ -1412,7 +1414,8 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId, supportChat = Nothing, memberPubKey = Nothing, relayLink = Nothing, - memberVerifiedCode = Nothing + memberVerifiedCode = Nothing, + memberBadgeProof = NoJSON Nothing } where insertMember_ = do @@ -1894,8 +1897,8 @@ setGroupInProgressDone db GroupInfo {groupId} = do "UPDATE groups SET creating_in_progress = 0, updated_at = ? WHERE group_id = ?" (currentTs, groupId) -createRelayRequestGroup :: DB.Connection -> StoreCxt -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> GroupMemberStatus -> RelayStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) -createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange initialDelay memberStatus relayStatus = do +createRelayRequestGroup :: DB.Connection -> StoreCxt -> User -> GroupRelayInvitation -> Maybe ProofPresHeader -> InvitationId -> VersionRangeChat -> Int64 -> GroupMemberStatus -> RelayStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink, publicGroupId} presHeader_ invId reqChatVRange initialDelay memberStatus relayStatus = do currentTs <- liftIO getCurrentTime -- Create group with placeholder profile let Profile {displayName = fromMemberLDN} = fromMemberProfile @@ -1920,7 +1923,7 @@ createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMemb g <- getGroupInfo db cxt user groupId pure (g, ownerMember) where - setRelayRequestData_ groupId currentTs = + setRelayRequestData_ groupId currentTs = do DB.execute db [sql| @@ -1934,10 +1937,15 @@ createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMemb WHERE group_id = ? |] (Binary invId, groupLink, minVersion reqChatVRange, maxVersion reqChatVRange, initialDelay, currentTs, groupId) + forM_ publicGroupId $ \gId -> + DB.execute + db + "UPDATE group_profiles SET public_group_id = ? WHERE group_profile_id = (SELECT group_profile_id FROM groups WHERE group_id = ?)" + (gId, groupId) insertOwner_ currentTs groupId = do let MemberIdRole {memberId, memberRole} = fromMember VersionRange minV maxV = reqChatVRange - (localDisplayName, profileId, _, _) <- createNewMemberProfile_ db cxt user fromMemberProfile Nothing currentTs + (localDisplayName, profileId, Profile {badge}, _) <- createNewMemberProfile_ db cxt user fromMemberProfile presHeader_ currentTs indexInGroup <- getUpdateNextIndexInGroup_ db groupId liftIO $ do DB.execute @@ -1953,7 +1961,9 @@ createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMemb :. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) :. (minV, maxV) ) - insertedRowId db + ownerMemberId <- insertedRowId db + forM_ badge $ createMemberBadgeProof db ownerMemberId + pure ownerMemberId updateRelayOwnStatusFromTo :: DB.Connection -> GroupInfo -> RelayStatus -> RelayStatus -> IO GroupInfo updateRelayOwnStatusFromTo db gInfo@GroupInfo {groupId} fromStatus toStatus = do @@ -2049,7 +2059,7 @@ getGroupViaPublicGroupId db User {userId} publicGroupId = SELECT g.group_id, gp.group_link FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - WHERE g.user_id = ? AND gp.public_group_id = ? + WHERE g.user_id = ? AND gp.public_group_id = ? AND gp.group_link IS NOT NULL LIMIT 1 |] (userId, publicGroupId) @@ -2091,7 +2101,7 @@ createJoiningMember "INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, contact_link, user_id, created_at, updated_at, badge_proof, badge_pres_header, badge_expiry, badge_type, badge_verified, badge_extra, badge_master_key, badge_signature, badge_key_idx, preferences, preferences_json) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" ((displayName, fullName, shortDescr, description, image, contactLink, userId, currentTs, currentTs) :. badgeToRow badge badgeVerified :. prefsToRow preferences) profileId <- liftIO $ insertedRowId db - case cReqMemberId_ of + r@(groupMemberId, _) <- case cReqMemberId_ of Just memberId -> do checkMemberNotExists memberId insertMember_ ldn profileId memberId currentTs @@ -2102,6 +2112,8 @@ createJoiningMember insertMember_ ldn profileId (MemberId memId) currentTs groupMemberId <- liftIO $ insertedRowId db pure (groupMemberId, MemberId memId) + liftIO $ forM_ badge $ createMemberBadgeProof db groupMemberId + pure r where VersionRange minV maxV = cReqChatVRange -- TODO [relays] relay: TBC communicate rejection @@ -2452,7 +2464,7 @@ createNewMember_ User {userId, userContactId} GroupInfo {groupId} NewGroupMember - { memInfo = MemberInfo memberId memberRole memChatVRange memberProfile memKey, + { memInfo = MemberInfo memberId memberRole memChatVRange memberProfile@Profile {badge} memKey, memCategory = memberCategory, memStatus = memberStatus, memRestriction, @@ -2486,6 +2498,7 @@ createNewMember_ :. (minV, maxV) ) groupMemberId <- liftIO $ insertedRowId db + liftIO $ forM_ badge $ createMemberBadgeProof db groupMemberId pure GroupMember { groupMemberId, @@ -2510,7 +2523,8 @@ createNewMember_ supportChat = Nothing, memberPubKey, relayLink = Nothing, - memberVerifiedCode = Nothing + memberVerifiedCode = Nothing, + memberBadgeProof = NoJSON badge } checkGroupMemberHasItems :: DB.Connection -> User -> GroupMember -> IO (Maybe ChatItemId) @@ -3418,7 +3432,8 @@ updateMemberProfile db cxt user@User {userId} m presHeader_ p = do currentTs <- liftIO getCurrentTime (p', badgeVerified) <- liftIO $ profileBadgeVerified presHeader_ (badgeKeys cxt) (Just $ memberProfile m) p let memberProfile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing - updateMemberProfile' currentTs p' badgeVerified memberProfile + m' <- updateMemberProfile' currentTs p' badgeVerified memberProfile + liftIO $ setMemberBadgeProof db m' presHeader_ p where GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m Profile {displayName = newName} = p @@ -3441,7 +3456,8 @@ updateContactMemberProfile db cxt user@User {userId} m ct@Contact {contactId} pr currentTs <- liftIO getCurrentTime (p', badgeVerified) <- liftIO $ profileBadgeVerified presHeader_ (badgeKeys cxt) (Just $ memberProfile m) p let profile = toLocalProfile profileId p' localAlias currentTs badgeVerified Nothing - updateContactMemberProfile' currentTs p' badgeVerified profile + (m', ct') <- updateContactMemberProfile' currentTs p' badgeVerified profile + (,ct') <$> liftIO (setMemberBadgeProof db m' presHeader_ p) where GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m Profile {displayName = newName} = p @@ -3455,6 +3471,22 @@ updateContactMemberProfile db cxt user@User {userId} m ct@Contact {contactId} pr updateContactLDN_ db user contactId localDisplayName ldn currentTs pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact) +setMemberBadgeProof :: DB.Connection -> GroupMember -> Maybe ProofPresHeader -> Profile -> IO GroupMember +setMemberBadgeProof db m@GroupMember {groupMemberId} presHeader_ Profile {badge} = case badge of + Just b | not (acceptedProof presHeader_ b) -> pure m + _ -> do + DB.execute db "DELETE FROM file_badge_proofs WHERE group_member_id = ?" (Only groupMemberId) + forM_ badge $ createMemberBadgeProof db groupMemberId + pure m {memberBadgeProof = NoJSON badge} + +createMemberBadgeProof :: DB.Connection -> GroupMemberId -> BadgeProof -> IO () +createMemberBadgeProof db groupMemberId badge = do + currentTs <- getCurrentTime + DB.execute + db + "INSERT INTO file_badge_proofs (group_member_id, proof_kind, badge_proof, badge_pres_header, badge_key_idx, badge_type, badge_expiry, badge_extra, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)" + ((groupMemberId, BPKMember) :. badgeProofToRow badge :. (currentTs, currentTs)) + getXGrpLinkMemReceived :: DB.Connection -> GroupMemberId -> ExceptT StoreError IO Bool getXGrpLinkMemReceived db mId = ExceptT . firstRow fromOnlyBI (SEGroupMemberNotFound mId) $ diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 392d499dde..8bed643b01 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -54,6 +54,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges import Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors import Simplex.Chat.Store.Postgres.Migrations.M20260923_preferences_json import Simplex.Chat.Store.Postgres.Migrations.M20260924_conn_pres_header +import Simplex.Chat.Store.Postgres.Migrations.M20260925_member_badge_proofs import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -107,7 +108,8 @@ schemaMigrations = ("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges), ("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors), ("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json), - ("20260924_conn_pres_header", m20260924_conn_pres_header, Just down_m20260924_conn_pres_header) + ("20260924_conn_pres_header", m20260924_conn_pres_header, Just down_m20260924_conn_pres_header), + ("20260925_member_badge_proofs", m20260925_member_badge_proofs, Just down_m20260925_member_badge_proofs) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20260925_member_badge_proofs.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20260925_member_badge_proofs.hs new file mode 100644 index 0000000000..999d17edb0 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20260925_member_badge_proofs.hs @@ -0,0 +1,29 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20260925_member_badge_proofs where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20260925_member_badge_proofs :: Text +m20260925_member_badge_proofs = + [r| +ALTER TABLE file_badge_proofs ALTER COLUMN file_id DROP NOT NULL; + +ALTER TABLE file_badge_proofs ADD COLUMN group_member_id BIGINT REFERENCES group_members ON DELETE CASCADE; + +CREATE UNIQUE INDEX idx_file_badge_proofs_group_member_id ON file_badge_proofs(group_member_id); +|] + +down_m20260925_member_badge_proofs :: Text +down_m20260925_member_badge_proofs = + [r| +DROP INDEX idx_file_badge_proofs_group_member_id; + +DELETE FROM file_badge_proofs WHERE group_member_id IS NOT NULL; + +ALTER TABLE file_badge_proofs DROP COLUMN group_member_id; + +ALTER TABLE file_badge_proofs ALTER COLUMN file_id SET NOT NULL; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 78f41ae410..51541d5175 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -875,7 +875,7 @@ ALTER TABLE test_chat_schema.extra_xftp_file_descriptions ALTER COLUMN extra_fil CREATE TABLE test_chat_schema.file_badge_proofs ( badge_proof_id bigint NOT NULL, - file_id bigint NOT NULL, + file_id bigint, proof_kind text NOT NULL, badge_proof bytea NOT NULL, badge_pres_header bytea NOT NULL, @@ -884,7 +884,8 @@ CREATE TABLE test_chat_schema.file_badge_proofs ( badge_expiry timestamp with time zone NOT NULL, badge_extra text NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + group_member_id bigint ); @@ -2629,6 +2630,10 @@ CREATE UNIQUE INDEX idx_file_badge_proofs_file_id_kind ON test_chat_schema.file_ +CREATE UNIQUE INDEX idx_file_badge_proofs_group_member_id ON test_chat_schema.file_badge_proofs USING btree (group_member_id); + + + CREATE INDEX idx_files_chat_item_id ON test_chat_schema.files USING btree (chat_item_id); @@ -3325,6 +3330,11 @@ ALTER TABLE ONLY test_chat_schema.file_badge_proofs +ALTER TABLE ONLY test_chat_schema.file_badge_proofs + ADD CONSTRAINT file_badge_proofs_group_member_id_fkey FOREIGN KEY (group_member_id) REFERENCES test_chat_schema.group_members(group_member_id) ON DELETE CASCADE; + + + ALTER TABLE ONLY test_chat_schema.files ADD CONSTRAINT files_contact_id_fkey FOREIGN KEY (contact_id) REFERENCES test_chat_schema.contacts(contact_id) ON DELETE CASCADE; diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 1bb05c4e94..182fc8e6fe 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -177,6 +177,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges import Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors import Simplex.Chat.Store.SQLite.Migrations.M20260923_preferences_json import Simplex.Chat.Store.SQLite.Migrations.M20260924_conn_pres_header +import Simplex.Chat.Store.SQLite.Migrations.M20260925_member_badge_proofs import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -353,7 +354,8 @@ schemaMigrations = ("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges), ("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors), ("20260923_preferences_json", m20260923_preferences_json, Just down_m20260923_preferences_json), - ("20260924_conn_pres_header", m20260924_conn_pres_header, Just down_m20260924_conn_pres_header) + ("20260924_conn_pres_header", m20260924_conn_pres_header, Just down_m20260924_conn_pres_header), + ("20260925_member_badge_proofs", m20260925_member_badge_proofs, Just down_m20260925_member_badge_proofs) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260925_member_badge_proofs.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260925_member_badge_proofs.hs new file mode 100644 index 0000000000..3738fc08d5 --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260925_member_badge_proofs.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20260925_member_badge_proofs where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20260925_member_badge_proofs :: Query +m20260925_member_badge_proofs = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'file_id INTEGER NOT NULL REFERENCES files', 'file_id INTEGER REFERENCES files') +WHERE name = 'file_badge_proofs' AND type = 'table'; + +PRAGMA writable_schema=RESET; + +ALTER TABLE file_badge_proofs ADD COLUMN group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE; + +CREATE UNIQUE INDEX idx_file_badge_proofs_group_member_id ON file_badge_proofs(group_member_id); +|] + +down_m20260925_member_badge_proofs :: Query +down_m20260925_member_badge_proofs = + [sql| +DROP INDEX idx_file_badge_proofs_group_member_id; + +DELETE FROM file_badge_proofs WHERE group_member_id IS NOT NULL; + +ALTER TABLE file_badge_proofs DROP COLUMN group_member_id; + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace(sql, 'file_id INTEGER REFERENCES files', 'file_id INTEGER NOT NULL REFERENCES files') +WHERE name = 'file_badge_proofs' AND type = 'table'; + +PRAGMA writable_schema=RESET; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index b3fc1569b8..c1387f1a67 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -30,6 +30,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -84,6 +85,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -169,13 +171,15 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.short_descr, p.description, p.image, p.contact_link, p.chat_peer_type, p.local_alias, p.preferences, p.preferences_json, p.badge_proof, p.badge_pres_header, p.badge_expiry, p.badge_type, p.badge_verified, p.badge_extra, p.badge_master_key, p.badge_signature, p.badge_key_idx, p.contact_domain, p.contact_domain_proof, p.contact_domain_verified, m.created_at, m.updated_at, - m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at + m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, m.member_pub_key, m.relay_link, m.member_security_code, m.member_security_code_verified_at, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id JOIN group_profiles gp USING (group_profile_id) JOIN group_members mu ON g.group_id = mu.group_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? AND mu.member_status NOT IN (?,?,?) @@ -186,6 +190,7 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH gp USING INTEGER PRIMARY KEY (rowid=?) SEARCH mu USING INDEX idx_group_members_contact_id (contact_id=?) SEARCH pu USING INTEGER PRIMARY KEY (rowid=?) +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT delivery_task_id @@ -292,6 +297,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -328,6 +334,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -364,6 +371,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -588,6 +596,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -623,6 +632,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -659,6 +669,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -695,6 +706,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1258,6 +1270,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1295,6 +1308,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1944,6 +1958,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -1980,6 +1995,7 @@ Query: VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -3925,7 +3941,7 @@ Query: SELECT g.group_id, gp.group_link FROM groups g JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id - WHERE g.user_id = ? AND gp.public_group_id = ? + WHERE g.user_id = ? AND gp.public_group_id = ? AND gp.group_link IS NOT NULL LIMIT 1 Plan: @@ -5879,10 +5895,12 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.user_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_status IN (?, ?, ?, ?) @@ -5896,6 +5914,7 @@ LIST SUBQUERY 1 SCAN chat_items USING COVERING INDEX idx_chat_items_group_member_id SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -5907,16 +5926,19 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id JOIN group_member_status_predicates sp ON m.member_status = sp.member_status WHERE m.group_id = ? AND m.relay_link = ? AND sp.current_member = 1 Plan: SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=?) SEARCH sp USING INDEX sqlite_autoindex_group_member_status_predicates_1 (member_status=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -5928,15 +5950,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ? Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -5948,15 +5973,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.index_in_group = ? Plan: SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=? AND index_in_group=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -5968,15 +5996,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.index_in_group = ? AND (m.member_role IN (?,?,?) OR m.group_member_id = ?) Plan: SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=? AND index_in_group=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -5988,15 +6019,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.member_category = ? Plan: SEARCH m USING INDEX idx_group_members_group_id_index_in_group (group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6008,15 +6042,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_id = ? AND m.member_id = ? Plan: SEARCH m USING INDEX sqlite_autoindex_group_members_1 (group_id=? AND member_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6028,15 +6065,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_member_id = ? AND m.user_id = ? Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6048,15 +6088,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.group_member_id = ? AND m.user_id = ? AND m.member_status NOT IN (?,?,?,?) Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6068,15 +6111,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) Plan: SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6088,15 +6134,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ? Plan: SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6108,15 +6157,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?) Plan: SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6128,15 +6180,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?) Plan: SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6148,15 +6203,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.user_id = ? AND m.group_id = ? AND m.contact_id IS DISTINCT FROM ? AND m.member_role = ? Plan: SEARCH m USING INDEX idx_group_members_group_id (user_id=? AND group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT @@ -6168,15 +6226,18 @@ Query: c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id WHERE m.user_id = ? AND m.removed_at < ? Plan: SEARCH m USING INDEX idx_group_members_user_id (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_group_member_id (group_member_id=?) LEFT-JOIN +SEARCH bp USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) LEFT-JOIN Query: SELECT f.file_id, f.ci_file_status, f.file_path @@ -6863,6 +6924,10 @@ SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND loca SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +Query: DELETE FROM file_badge_proofs WHERE group_member_id = ? +Plan: +SEARCH file_badge_proofs USING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) + Query: DELETE FROM files WHERE roster_transfer_id = ? Plan: SEARCH files USING COVERING INDEX idx_files_roster_transfer_id (roster_transfer_id=?) @@ -6893,6 +6958,7 @@ SEARCH files USING COVERING INDEX idx_files_redirect_file_id (redirect_file_id=? Query: DELETE FROM group_members WHERE member_id = ? Plan: SCAN group_members +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -6924,6 +6990,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM group_members WHERE user_id = ? AND group_id = ? Plan: SEARCH group_members USING COVERING INDEX idx_group_members_group_id (user_id=? AND group_id=?) +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -6955,6 +7022,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_group_member_id (conta Query: DELETE FROM group_members WHERE user_id = ? AND group_member_id = ? Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +SEARCH file_badge_proofs USING COVERING INDEX idx_file_badge_proofs_group_member_id (group_member_id=?) SEARCH rcv_roster_transfers USING COVERING INDEX idx_rcv_roster_transfers_from_member_id (from_member_id=?) SEARCH group_relays USING COVERING INDEX idx_group_relays_group_member_id (group_member_id=?) SEARCH delivery_jobs USING COVERING INDEX idx_delivery_jobs_job_scope_support_gm_id (job_scope_support_gm_id=?) @@ -7192,6 +7260,9 @@ SEARCH users USING INTEGER PRIMARY KEY (rowid=?) Query: INSERT INTO extra_xftp_file_descriptions (file_id, user_id, file_descr_text, created_at, updated_at) VALUES (?,?,?,?,?) Plan: +Query: INSERT INTO file_badge_proofs (group_member_id, proof_kind, badge_proof, badge_pres_header, badge_key_idx, badge_type, badge_expiry, badge_extra, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?) +Plan: + Query: INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, redirect_file_id, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -8064,6 +8135,12 @@ Query: UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE g Plan: SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE group_profiles SET public_group_id = ? WHERE group_profile_id = (SELECT group_profile_id FROM groups WHERE group_id = ?) +Plan: +SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) +SCALAR SUBQUERY 1 +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_relay_id = ? Plan: SEARCH group_relays USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index d66848e42a..63659a129d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -860,7 +860,7 @@ CREATE TABLE rcv_roster_transfers( ) STRICT; CREATE TABLE file_badge_proofs( badge_proof_id INTEGER PRIMARY KEY AUTOINCREMENT, - file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, + file_id INTEGER REFERENCES files ON DELETE CASCADE, proof_kind TEXT NOT NULL, badge_proof BLOB NOT NULL, badge_pres_header BLOB NOT NULL, @@ -870,6 +870,8 @@ CREATE TABLE file_badge_proofs( badge_extra TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL + , + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE ) STRICT; CREATE TABLE invoices( invoice_id TEXT NOT NULL PRIMARY KEY, @@ -1545,6 +1547,9 @@ CREATE INDEX idx_badge_code_redemptions_user ON badge_code_redemptions( CREATE UNIQUE INDEX idx_badge_purchases_code_redemption ON badge_purchases( badge_code_redemption_id ); +CREATE UNIQUE INDEX idx_file_badge_proofs_group_member_id ON file_badge_proofs( + group_member_id +); CREATE TRIGGER on_group_members_insert_update_summary AFTER INSERT ON group_members FOR EACH ROW diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 4f0c0b6589..a952d20670 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -32,7 +32,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Type.Equality -import Simplex.Chat.Badges (BadgeRow, ProofPresHeader, badgeToRow, rowToBadge) +import Simplex.Chat.Badges (BadgeRow, MaybeBadgeProofRow, ProofPresHeader, badgeToRow, maybeRowToBadgeProof, rowToBadge) import Simplex.Chat.Names (SimplexDomainProof, SimplexDomainClaim (..), claimDomain) import Simplex.Chat.Messages import Simplex.Chat.Remote.Types @@ -693,9 +693,9 @@ type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) +type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519, Maybe B64UrlByteString) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519) :. GroupMemberRow type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof) @@ -704,7 +704,7 @@ type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, Ver type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Text, Maybe Text) :. BadgeRow :. ContactDomainRow toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> (GroupInfo, GroupKeysRow) -toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) = +toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. (rootPrivKey_, rootPubKey_, memberPrivKey_) :. userMemberRow) = let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} groupPreferences = groupPrefsFromRow encodedPrefs receivedPrefs @@ -715,7 +715,7 @@ toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayN preparedGroup = toPreparedGroup preparedGroupRow groupSummary = GroupSummary {currentMembers, publicMemberCount} gInfo = GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupDomainVerified = unBI <$> groupDomainVerified} - in (gInfo, groupKeysRow) + in (gInfo, (rootPrivKey_, rootPubKey_, memberPrivKey_, publicGroupId_)) toGroupInfo_ :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo toGroupInfo_ now cxt userContactId chatTags row = fst $ toGroupInfo now cxt userContactId chatTags row @@ -747,7 +747,7 @@ toPublicGroupAccess (groupWebPage, groupDomain_, domainWebPage_, allowEmbedding_ allowEmbedding = maybe False unBI allowEmbedding_ mkGroupKeys :: DB.Connection -> StoreCxt -> GroupInfo -> GroupKeysRow -> ExceptT StoreError IO GroupKeys -mkGroupKeys db cxt g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup}, membership} (rootPrivKey, rootPubKey, memberPrivKey_) = do +mkGroupKeys db cxt g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup}, membership} (rootPrivKey, rootPubKey, memberPrivKey_, publicGroupId) = do memberPrivKey <- case memberPrivKey_ of Just k -> pure k Nothing -> do @@ -757,7 +757,7 @@ mkGroupKeys db cxt g@GroupInfo {groupId, groupProfile = GroupProfile {publicGrou (False, _, _) -> GKGroup {memberPrivKey} (True, True, Just groupRootKey) -> GKPublicGroup {groupRootKey, memberPrivKey} (True, True, Nothing) -> GKPreparedPublicGroup {memberPrivKey} - (True, False, _) -> GKRelayRequest {memberPrivKey} + (True, False, _) -> GKRelayRequest {memberPrivKey, publicGroupId} setUserMemberKey :: DB.Connection -> GroupId -> GroupMemberId -> C.PrivateKeyEd25519 -> ExceptT StoreError IO C.PrivateKeyEd25519 setUserMemberKey db groupId membershipId newKey = do @@ -784,6 +784,7 @@ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId invitedBy = toInvitedBy userContactId invitedById activeConn = Nothing memberVerifiedCode = SecurityCode <$> memberCode_ <*> memberCodeVerifiedAt_ + memberBadgeProof = NoJSON Nothing memberChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer supportChat = case supportChatTs_ of Just chatTs -> @@ -810,15 +811,17 @@ groupMemberQuery = c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, + bp.badge_proof, bp.badge_pres_header, bp.badge_key_idx, bp.badge_type, bp.badge_expiry, bp.badge_extra FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.group_member_id = m.group_member_id + LEFT JOIN file_badge_proofs bp ON bp.group_member_id = m.group_member_id |] -toContactMember :: UTCTime -> StoreCxt -> User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember -toContactMember now cxt User {userContactId} (memberRow :. connRow) = - (toGroupMember now userContactId memberRow) {activeConn = toMaybeConnection cxt connRow} +toContactMember :: UTCTime -> StoreCxt -> User -> (GroupMemberRow :. MaybeConnectionRow :. MaybeBadgeProofRow) -> GroupMember +toContactMember now cxt User {userContactId} (memberRow :. connRow :. proofRow) = + (toGroupMember now userContactId memberRow) {activeConn = toMaybeConnection cxt connRow, memberBadgeProof = NoJSON $ maybeRowToBadgeProof proofRow} rowToLocalProfile :: UTCTime -> ProfileRow -> LocalProfile rowToLocalProfile now ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, encodedPrefs, receivedPrefs) :. badgeRow :. domainRow) = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 6734789911..44f239f492 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -490,7 +490,8 @@ data GroupKeys memberPrivKey :: C.PrivateKeyEd25519 } | GKRelayRequest - { memberPrivKey :: C.PrivateKeyEd25519 + { memberPrivKey :: C.PrivateKeyEd25519, + publicGroupId :: Maybe B64UrlByteString } | GKPreparedPublicGroup { memberPrivKey :: C.PrivateKeyEd25519 @@ -1008,7 +1009,8 @@ data GroupRelayInvitation = GroupRelayInvitation { fromMember :: MemberIdRole, fromMemberProfile :: Profile, relayMemberId :: MemberId, - groupLink :: ShortLinkContact + groupLink :: ShortLinkContact, + publicGroupId :: Maybe B64UrlByteString } deriving (Eq, Show) @@ -1212,7 +1214,8 @@ data GroupMember = GroupMember relayLink :: Maybe ShortLinkContact, -- out-of-band verified security code for connectionless (channel) members; -- regular members carry it in activeConn instead (see memberSecurityCode) - memberVerifiedCode :: Maybe SecurityCode + memberVerifiedCode :: Maybe SecurityCode, + memberBadgeProof :: NoJSON BadgeProof } deriving (Eq, Show) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index 7445a9c2d5..ae257b5025 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -151,20 +151,22 @@ setPreference_ f pref_ prefs = SCFCalls -> prefs {calls = pref_} SCFSessions -> prefs {sessions = pref_} -newtype PrefsJSON = PrefsJSON {unPrefsJSON :: Maybe Object} +newtype NoJSON a = NoJSON {unNoJSON :: Maybe a} deriving (Eq, Show) -instance ToJSON PrefsJSON where +instance ToJSON (NoJSON a) where toJSON _ = Null toEncoding _ = JE.null_ omitField _ = True -instance FromJSON PrefsJSON where - parseJSON _ = pure $ PrefsJSON Nothing - omittedField = Just $ PrefsJSON Nothing +instance FromJSON (NoJSON a) where + parseJSON _ = pure $ NoJSON Nothing + omittedField = Just $ NoJSON Nothing + +type PrefsJSON = NoJSON Object keepPrefsJSON :: (ToJSON p, HasField "_json" p PrefsJSON) => Value -> p -> p -keepPrefsJSON v ps = setField @"_json" ps . PrefsJSON $ case v of +keepPrefsJSON v ps = setField @"_json" ps . NoJSON $ case v of Object o | v /= toJSON ps -> Just o _ -> Nothing @@ -177,7 +179,7 @@ prefsFromRow_ prefsP encodedPrefs receivedPrefs = (decode =<< receivedPrefs) <|> decode = decodePrefs prefsP prefsToRow :: HasField "_json" p PrefsJSON => Maybe p -> (Maybe p, Maybe Text) -prefsToRow ps = (ps, encodeJSON . Object <$> (unPrefsJSON . getField @"_json" =<< ps)) +prefsToRow ps = (ps, encodeJSON . Object <$> (unNoJSON . getField @"_json" =<< ps)) -- collection of optional chat preferences for the user and the contact data Preferences = Preferences @@ -545,7 +547,7 @@ toChatPrefs FullPreferences {timedMessages, fullDelete, reactions, voice, files, calls = Just calls, sessions = Just sessions, commands = Just cmds, - _json = PrefsJSON Nothing + _json = NoJSON Nothing } defaultChatPrefs :: FullPreferences @@ -562,7 +564,7 @@ defaultChatPrefs = } emptyChatPrefs :: Preferences -emptyChatPrefs = Preferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing (PrefsJSON Nothing) +emptyChatPrefs = Preferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing (NoJSON Nothing) defaultGroupPrefs :: FullGroupPreferences defaultGroupPrefs = @@ -584,7 +586,7 @@ defaultGroupPrefs = } emptyGroupPrefs :: GroupPreferences -emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing (PrefsJSON Nothing) +emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing (NoJSON Nothing) businessGroupPrefs :: Preferences -> GroupPreferences businessGroupPrefs Preferences {timedMessages, fullDelete, reactions, voice, files, sessions, commands} = @@ -620,7 +622,7 @@ defaultBusinessGroupPrefs = comments = Just $ CommentsGroupPreference FEOff Nothing, signMessages = Just $ SignMessagesGroupPreference FEOff, commands = Nothing, - _json = PrefsJSON Nothing + _json = NoJSON Nothing } data TimedMessagesPreference = TimedMessagesPreference @@ -1133,7 +1135,7 @@ toGroupPreferences groupPreferences@FullGroupPreferences {commands = ListDef cmd comments = pref SGFComments, signMessages = pref SGFSignMessages, commands = Just cmds, - _json = PrefsJSON Nothing + _json = NoJSON Nothing } where pref :: SGroupFeature f -> Maybe (GroupFeaturePreference f) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 02fa3e49b1..139b9ba376 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -76,6 +76,7 @@ chatProfileTests = do it "supporter badge bound to another chat is ignored, stored badge is kept" testUserBadgeOtherBinding it "supporter badge of member joining via group link, at request and after handshake" testUserBadgeGroupLinkJoiner it "supporter badge of introduced member" testUserBadgeIntroduced + it "supporter badge of member invited via contact, forwarded to introduced member" testUserBadgeInvitedIntroduced it "supporter badge in one-time link data" testUserBadgeInvitationLinkData it "supporter badge in data of address getting its first short link" testUserBadgeAddressFirstShortLink it "supporter badge in shared address card" testUserBadgeAddressCard @@ -800,6 +801,34 @@ testUserBadgeIntroduced ps = do cath <# "#team alice> hello" memberBadgeHeader cath "team" "bob" `shouldReturn` Just ("CG", BSActive) +testUserBadgeInvitedIntroduced :: HasCallStack => TestParams -> IO () +testUserBadgeInvitedIntroduced ps = do + Right (pk, sk) <- bbsKeyGen + testChatCfg3 (testCfg {badgePublicKeys = testBadgeKeys pk}) aliceProfile bobProfile cathProfile (test sk) ps + where + test sk alice bob cath = do + createGroup2 "team" alice cath + connectUsers alice bob + addTestBadge bob =<< issueTestBadge sk futureDate + bob #> "@alice hi" + alice <# "bob *> hi" + cath ##> "/_stop" + cath <## "chat stopped" + addMember "team" alice bob GRAdmin + bob ##> "/j team" + concurrently_ + (alice <## "#team: bob joined the group") + (bob <## "#team: you joined the group") + memberBadgeHeader alice "team" "bob" `shouldReturn` Just ("CD", BSActive) + memberProofHeader alice "team" "bob" `shouldReturn` Just "CG" + bob ##> "/_stop" + bob <## "chat stopped" + cath ##> "/_start" + cath <## "chat started" + cath <## "subscribed 2 connections on server localhost" + cath <## "#team: alice added bob (Bob) to the group (connecting...)" + memberBadgeHeader cath "team" "bob" `shouldReturn` Just ("CG", BSActive) + testUserBadgeInvitationLinkData :: HasCallStack => TestParams -> IO () testUserBadgeInvitationLinkData ps = do Right (pk, sk) <- bbsKeyGen diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 8d7a2dbc63..3995a5c51e 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -130,9 +130,9 @@ versionTestMatrix2 runTest = do it "prev" $ runTestCfg2 testCfgVPrev testCfgVPrev (runTest True True) it "prev to curr" $ runTestCfg2 testCfg testCfgVPrev (runTest True True) it "curr to prev" $ runTestCfg2 testCfgVPrev testCfg (runTest True True) - it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest True False) + it "old (1st supported)" $ testChatCfg2 testCfgV1 aliceProfile bobProfile (runTest True True) it "old to curr" $ runTestCfg2 testCfg testCfgV1 (runTest True True) - it "curr to old" $ runTestCfg2 testCfgV1 testCfg (runTest True False) + it "curr to old" $ runTestCfg2 testCfgV1 testCfg (runTest True True) versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith TestParams versionTestMatrix3 runTest = do @@ -732,7 +732,13 @@ requestBadgeHeader cc cName = >>= either (fail . show) (\UserContactRequest {profile} -> pure $ storedBadgeHeader profile) memberBadgeHeader :: TestCC -> GroupName -> ContactName -> IO (Maybe (String, BadgeStatus)) -memberBadgeHeader cc gName mName = +memberBadgeHeader cc gName mName = (\GroupMember {memberProfile} -> storedBadgeHeader memberProfile) <$> getTestMember cc gName mName + +memberProofHeader :: TestCC -> GroupName -> ContactName -> IO (Maybe String) +memberProofHeader cc gName mName = (\GroupMember {memberBadgeProof} -> proofHeaderTag <$> unNoJSON memberBadgeProof) <$> getTestMember cc gName mName + +getTestMember :: TestCC -> GroupName -> ContactName -> IO GroupMember +getTestMember cc gName mName = withCCTransaction cc $ \db -> withCCUser cc $ \user -> runExceptT @@ -740,7 +746,7 @@ memberBadgeHeader cc gName mName = gId <- getGroupIdByName db user gName getGroupMember db (storeCxt $ chatController cc) user gId =<< getGroupMemberIdByName db user gId mName ) - >>= either (fail . show) (\GroupMember {memberProfile} -> pure $ storedBadgeHeader memberProfile) + >>= either (fail . show) pure lastItemContent :: TestCC -> IO String lastItemContent cc = @@ -751,15 +757,16 @@ lastItemContent cc = storedBadgeHeader :: LocalProfile -> Maybe (String, BadgeStatus) storedBadgeHeader LocalProfile {localBadge} = case localBadge of - Just (PeerBadge (BadgeProof _ (BBSPresHeader ph) _ _) st) -> Just (headerTag $ strDecode ph, st) + Just (PeerBadge b st) -> Just (proofHeaderTag b, st) _ -> Nothing - where - headerTag = \case - Right (PHChat b) -> 'C' : take 1 (B.unpack b) - Right (PHRequest _) -> "R" - Right (PHLink _) -> "L" - Right (PHTest _) -> "T" - _ -> "?" + +proofHeaderTag :: BadgeProof -> String +proofHeaderTag (BadgeProof _ (BBSPresHeader ph) _ _) = case strDecode ph of + Right (PHChat b) -> 'C' : take 1 (B.unpack b) + Right (PHRequest _) -> "R" + Right (PHLink _) -> "L" + Right (PHTest _) -> "T" + _ -> "?" lastItemId :: HasCallStack => TestCC -> IO String lastItemId cc = do diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index f05650143d..1d5bd3e18f 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -170,10 +170,10 @@ s #==# msg = do s ==# msg testChatPreferences :: Maybe Preferences -testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing, _json = PrefsJSON Nothing} +testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, files = Nothing, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}, sessions = Nothing, commands = Nothing, _json = NoJSON Nothing} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, signMessages = Nothing, commands = Nothing, _json = PrefsJSON Nothing} +testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing, reports = Nothing, support = Nothing, sessions = Nothing, comments = Nothing, signMessages = Nothing, commands = Nothing, _json = NoJSON Nothing} testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", shortDescr = Nothing, description = Nothing, image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), peerType = Nothing, contactLink = Nothing, preferences = testChatPreferences, badge = Nothing, contactDomain = Nothing}