mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 17:58:47 +00:00
badge profile presentations
This commit is contained in:
@@ -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, "", [], "", ""),
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 25b2734280f1c969f12d349c791a9943cca01168
|
||||
tag: bf391d60eca9813ceb2f7ef77d104960a463e46e
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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) =
|
||||
|
||||
@@ -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) $
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|]
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|]
|
||||
@@ -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=?)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) =
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-12
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user