distribute keys and sign

This commit is contained in:
Evgeny @ SimpleX Chat
2026-07-27 09:34:36 +00:00
parent 261d09ba42
commit 138a42f582
16 changed files with 351 additions and 82 deletions
+114
View File
@@ -0,0 +1,114 @@
# p2p group member keys - generation and distribution
## Goal
Give every member of a p2p (non-relay) group an Ed25519 signing key, and distribute each member's public key to the other members, so p2p group messages can be signed and verified. New members are keyed at join; existing members are keyed on upgrade and their keys are distributed through the existing profile-update path.
## Design (agreed)
- Own key: private in `groups.member_priv_key` (via `GroupKeys.memberPrivKey`), public in the membership's `group_members.member_pub_key`. `groupKeys = Just (GroupKeys {publicGroupKeys = Nothing, memberPrivKey})` marks a p2p member key.
- Distribution: the public key is included in `XInfo` (and in `XContact` at join). `XInfo` is sent by the existing profile-update send (`sendGroupProfileUpdate`); the key is included whenever `XInfo` is sent, and a per-member flag records delivery to version-compatible members. One `XInfo` per send - if the profile is sent because it changed, the key is included in that message rather than a second one.
- Version: a new chat version decides who is marked and who can read the key. A member between version 7 and the new version receives `XInfo` for the profile and ignores the unknown key field.
- No acknowledgement in groups: the flag is set on send. A lost message means the member cannot verify until the next send re-delivers the key; whether an unverifiable claim is hidden or shown is a per-claim decision.
## Current state (last commit `261d09ba4`)
Field plumbing is done: `memberKey :: Maybe MemberKey` added to `XInfo` and `XContact`, full encode/decode, all call sites pass `Nothing`/`_`. Four `TODO [member keys]` markers remain at the fill-in points: `Commands.hs:3911` (XContact join), `Internal.hs:2489` (profile-update send `sendGroupProfileUpdate`), `Subscriber.hs:836` (join-confirmation allow), `xInfoMember` (receive/store).
## Changes
### 1. Version - `Protocol.hs`
- Add `groupMemberKeyVersion :: VersionChat = VersionChat 20` with a comment.
- `currentChatVersion = VersionChat 20` (from 19).
- Add changelog line `-- 20 - p2p group member keys for signing (2026-07-26)`.
- No new binary-floor constant: reuse `relayWebCapVersion` (18) as the reliable binary-batch floor for partitioning signed sends (item 7). Binary parsing was added in #6597 at version 17 with no constant; 18 is the first guaranteed.
### 2. Schema + type + row parsing
- New migration `M20260726_member_key_sent.hs` (mirror `M20260720_server_roles.hs`): `ALTER TABLE group_members ADD COLUMN user_member_key_sent INTEGER NOT NULL DEFAULT 0`. Update `chat_schema.sql`.
- `GroupMember` (`Types.hs:1119`): add `userMemberKeySent :: Bool` after `memberPubKey`.
- `GroupMemberRow` / `MaybeGroupMemberRow` (`Groups.hs:263`): add `BoolInt` / `Maybe BoolInt` to the last tuple group, next to `member_pub_key`.
- `toGroupMember` / `toMaybeGroupMember`: parse the column.
- Every `SELECT` that builds a `GroupMemberRow` adds `user_member_key_sent` (shared column list - several sites; grep the existing `member_pub_key, relay_link` list).
### 3. Own key generation + storage (key exists before signing)
- New store fn `setUserMemberKey :: DB.Connection -> GroupId -> GroupMemberId -> C.PrivateKeyEd25519 -> IO ()`: two writes, both required - the private key to `groups.member_priv_key` (the user's own signing key for this group) and its derived public key to the user's own membership row (`group_members.member_pub_key`), as `createNewGroup:429` does.
- New helper `ensureUserMemberKey :: User -> GroupInfo -> CM GroupInfo`: if `groupKeys` already has a key, return `gInfo` unchanged; for a p2p group with `groupKeys = Nothing`, generate an Ed25519 key, store it via `setUserMemberKey`, and return `gInfo` with `groupKeys = Just (GroupKeys {publicGroupKeys = Nothing, memberPrivKey})`. Idempotent (check-and-set in one transaction so concurrent sends cannot create two keys).
- Generation points:
- Create group - `APINewGroup` (`Commands.hs:2642`): generate the key and pass `Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey}` to `newGroup` (was `Nothing`). `createNewGroup` (`Groups.hs:393-430`) already stores both columns.
- Join - in `joinContact`'s p2p-group branch (item 5): `gInfo' <- ensureUserMemberKey user gInfo`, take the public key from `gInfo'` `groupKeys` for `XContact`.
- Send - call `ensureUserMemberKey` at the top of the send entry (`sendGroupMessages` `:2458`, `sendGroupSignedMessages` `:2464`) and thread the returned `gInfo'` to BOTH `sendGroupProfileUpdate` and `sendGroupMessages_`, so `groupMsgSigning` signs the very first message after generation - the key must not lag one message behind. This is also the lazy path for groups created before this change.
### 4. `sendGroupProfileUpdate` - one `XInfo` with profile and/or key (`Internal.hs:2468`)
Key and signing are independent of incognito status: the member key is per group and needed in every p2p group. Only the badge may depend on incognito, and that is handled by the existing profile/badge logic, not the key path. `shouldSendProfileUpdate` gates only the profile part; the key part runs regardless. Restructure `sendGroupProfileUpdate` so a single `XInfo` serves both purposes (never two messages), for non-relay groups, using `gInfo'` from `ensureUserMemberKey`:
- `profileMembers = if shouldSendProfileUpdate then filter (\`supportsVersion\` memberProfileUpdateVersion) members else []` (unchanged trigger; still skips incognito, scope, asGroup).
- `keyMembers` = members with `supportsVersion groupMemberKeyVersion` and `not (userMemberKeySent m)` - runs regardless of incognito.
- recipients = union of the two.
- Send one `XInfo profile (Just ownKey)` to recipients via `sendGroupMessages_` (`:2500`), which returns the `GroupSndResult` needed for marking - `sendGroupMessage'` (`:2374`) discards it (the `_` at `:2377`), so the current `sendGroupProfileUpdate` send call must change. `profile` and its badge are the existing profile logic (unchanged); `ownKey = MemberKey (C.publicKey memberPrivKey)` from `gInfo'` `groupKeys`.
- After send, from that `GroupSndResult`: set `userMemberKeySent = True` for a key recipient whose `sentTo` delivery result (the third tuple element, `:2495`) is `Right`, or that is in `pending` or `forwarded` (enqueued, stored for delivery on connect, or forwarded). A `sentTo` `Left` (enqueue failure) stays `False`. `updateUserMemberProfileSentAt` only when `shouldSendProfileUpdate`.
- Retry stays but is uncapped: any `False` member is re-included on the next send. No cap is needed because `memberSendAction` (`Internal.hs:2608`) returns `Nothing` for a disabled/deleted/failed/rejected connection, so `addMember` (`:2542`) skips it - re-inclusion re-filters it in memory, it is never actually re-sent. The only un-marked-but-attempted case is a `sentTo` failure on a *ready* connection (a rare enqueue error), which retries next send and fails identically for the content message; a truly broken connection transitions to disabled/failed and is then skipped. So retry is cheap and self-limiting. `user_member_key_sent` is a plain boolean.
- New store fn `setMembersMemberKeySent :: DB.Connection -> [GroupMemberId] -> IO ()`.
- Relay groups keep current behaviour (no key here; key comes from the roster).
The member list is already in memory and `userMemberKeySent` is a field on the record, so both filters are in-memory with no extra query.
### 5. Fill the TODO send points
- `joinContact` (`Commands.hs:3900`): the key belongs only in the `Just (Just gInfo) | not (useRelays' gInfo)` case (p2p group join). Split that out of the current `_` branch: `gInfo' <- ensureUserMemberKey user gInfo`, then `XContact profileToSend (Just ownKey) (Just xContactId) welcomeSharedMsgId msg_`. The `Just Nothing` (unknown group) and `Nothing` (direct contact) cases keep `XContact ... Nothing ...`. `XContact` is `encodeConnInfoPQ` (JSON), so this delivery is **unsigned** - the initial trust-on-first-use key. The membership row exists in `gInfo` here, so `setUserMemberKey` writes `member_pub_key` (#5 confirmed).
- `Subscriber.hs:836` (joiner's allow-reply to the host): `XInfo profileToSend (Just ownKey)`, **signed** with the joiner's key when the host version allows (item 6). `XInfo` is `requiresSignature`, so it is signed like any other `XInfo`; this gives the host a signed confirmation of the joiner's key at join.
- `Subscriber.hs:1626` (host accepting the join): pass the parsed `XContact.memberKey` to `acceptGroupJoinRequestAsync` instead of `Nothing`; it flows to `createJoiningMember` (`Groups.hs:2070`, `:2112`), which stores `member_pub_key` (unsigned TOFU).
- Host key to the joiner: `XGrpLinkMem` (`Protocol.hs:503`, currently `Profile` only) needs a `Maybe MemberKey` field added, like the commit added to `XInfo`/`XContact`. The host already sends it during the join in `sendXGrpLinkMem` (`Subscriber.hs:974`), fired on the joiner's `CON` (`:958`); include the host's key, and store it in `xGrpLinkMem` (`:2760`). This is the host->joiner counterpart of the joiner's `XContact`. Add `XGrpLinkMem` to `requiresSignature` (safe - p2p-only, relay groups never send/receive it). But `sendXGrpLinkMem` currently uses `sendDirectMemberMessage` -> `sendDirectMessage_` -> `createSndMessage` (`:2197`), which hardcodes `Nothing` signing and sends via `deliverMessage` (no `groupMsgSigning`, no mode partition) - so `requiresSignature` alone would not sign it. Switch `sendXGrpLinkMem` to `sendGroupMemberMessages` (`:2228`), which computes `groupMsgSigning` (`:2231`) and uses the mode at `:2232` (item-7 partition site: binary-signed to a v20+ joiner, unsigned JSON to a pre-20 joiner), and run `ensureUserMemberKey` first so the host has a key to sign with. `xGrpLinkMem` (`:2760`) must `verifyGroupSig` against the key delivered in the message (self-certifying, like `:868`), since `withVerifiedMsg` has no stored host key yet.
### 6. Receive + confirm the key
The key is confirmed cryptographically wherever the `XInfo` is signed. Two receive points:
- Handshake allow-reply - `Subscriber.hs:868` (`XInfo _ _`). The joiner's reply can be signed: `encodeSignedConnInfo` already produces a signed connInfo (used by `encodeXMemberConnInfo`), and the peer version is known by `INFO` (`updatePeerChatVRange`), so sign the allow-reply (item 5) with the joiner's key when the host version supports it. `parseChatMessage` here is `parseChatMessage'` with the signature discarded (`Internal.hs:1793`); switch to `parseChatMessage'`, verify the signature against the key in the `XInfo`, then read and confirm the key. This confirms the joiner's key at join.
- Group-message `XInfo` - `xInfoMember` (`Subscriber.hs:2755`), signed via item 7, for ongoing profile/key updates.
Store/confirm rule at both points, new store fn `setMemberPubKey :: DB.Connection -> GroupMemberId -> C.PublicKeyEd25519 -> IO ()` (the key-only, no-role counterpart of the existing `setGroupMemberKeyRole`):
- `memberPubKey m = Nothing`, `mKey = Just k` -> store `k`.
- `memberPubKey m = Just k0` -> accept only `Nothing` or `Just k0`; a different key is rejected (immutable).
This is the same pin-or-reject rule as the existing `applyMemberKeyRole` (`Subscriber.hs`, used by the roster): `Nothing` -> pin, `Just k` with `k /= pubKey` -> `Left` reject. Reuse or mirror it.
Signing is uniform: `XInfo` is `requiresSignature`, so every group `XInfo` (the 836 allow-reply, group profile updates) and `XGrpLinkMem` is signed with the member's key when the recipient version allows (binary). The one unconditionally-unsigned delivery is `XContact` - a JSON connInfo that cannot be signed; the host holds that key as trust-on-first-use, confirmed by the joiner's signed `XInfo` (the 836 reply, then later updates).
### 7. Signed send - partition recipients by binary capability
Once `groupKeys = Just`, `groupMsgSigning` (`Internal.hs:2214`) produces a `MsgSigning` for p2p messages, and `createNewSndMessage` (`Store/Messages.hs:236`) stores each `SndMessage` with both `msgBody` (plain encoded message) and `signedMsg_ :: Maybe SignedMsg` (signature over that body). A signed element cannot sit in a JSON batch: `encodeBatchElement (Just sm) body = "/" <> smpEncode (chatBinding, signatures) <> body` (binary), `encodeBatchElement Nothing body = body` (plain JSON), and `encodeBatch` wraps them as `=...` (binary) or `[...]` (JSON) (`Batch.hs:70`, `:130-134`). So the same `SndMessage` yields either form with no re-encoding: keep `signedMsg_` for the signed element, set it to `Nothing` for the unsigned one.
The send path currently picks one mode for the whole group: `mode = if useRelays' gInfo then BMBinary else BMJson` (`Internal.hs:2232`, `:2549`, `:2676`), so p2p is always `BMJson`. Change, for a p2p group when any message is signed (`any (isJust . signedMsg_) msgs`):
- Partition the recipients (`toSendSeparate` and `toSendBatched`) by `\`supportsVersion\` relayWebCapVersion` (18, the binary-batch floor).
- Binary-capable members -> `batchSndMessagesJSON BMBinary msgs` (signed `/` elements).
- Binary-incapable members -> `batchSndMessagesJSON BMJson (map (fmap dropSig) msgs)`, `dropSig m = m {signedMsg_ = Nothing}` (unsigned JSON).
- Fold each partition over its own batch (`foldMembers` already runs per list) and concatenate; body references (`VRRef`) are naturally per-partition.
Relay groups (`BMBinary` for all) and unsigned p2p sends (`BMJson` for all) are unchanged.
A binary-capable member below `groupMemberKeyVersion` (18-19) receives the signed form, stores it unverified (no key), and can forward it intact via `encodeFwdElement` (`Batch.hs:125`), which preserves `signedMsg_` - which is why the partition is by binary capability, not key possession. A member below 18 receives the unsigned JSON form; in a p2p group `signatureOptional` is true, so it accepts the unsigned message rather than rejecting it.
Three mode sites to update: `prepareMsgReqs` (`:2549`, main group send), `sendGroupMemberMessages` (`:2232`, member-to-member / introductions), and `:2676`.
## Implementation status (2026-07-27)
All seven items implemented:
1. Version - `groupMemberKeyVersion = 20`, `currentChatVersion = 20`, `XGrpLinkMem` in `requiresSignature`, `XGrpLinkMem`/`XInfo`/`XContact` carry `Maybe MemberKey`.
2. Schema/type/rows - `user_member_key_sent` column (both backends, migration `M20260727_member_key_sent`), `userMemberKeySent :: Bool` on `GroupMember`, all `GroupMemberRow`/`MaybeGroupMemberRow` SELECTs and parses.
3. Generation - `setUserMemberKey`, `ensureUserMemberKey`, `groupMemberKey`; keyed at `APINewGroup` (create), `joinContact` (contact-link join), `sendXGrpLinkMem` and `XGrpLinkInv` handshake (prepared-group join), and lazily at the send entries.
4. `sendGroupProfileUpdate` - one `XInfo profile (Just ownKey)` to `profileMembers keyMembers` via `sendGroupMessages_`, marks `setMembersMemberKeySent` for delivered v20+ recipients (`sentTo` Right / `pending` / `forwarded`), `updateUserMemberProfileSentAt` only when the profile changed.
5. Distribution - `XContact` (joiner->host, TOFU) threaded through `profileContactRequest` to `acceptGroupJoinRequestAsync`; `XGrpLinkMem` (host->joiner) signed via `sendGroupMemberMessages`; `XInfo` allow-reply (joiner->host) signed via `encodeSignedGroupConnInfo` + `allowAgentConnectionInfo`, gated on `not (useRelays' gInfo) && maxVersion chatVRange >= relayWebCapVersion`.
6. Receive - `storeMemberKey` (pin-or-reject, self-certified by `verifyGroupSig`) at `xInfoMember`, `xGrpLinkMem`, and the `INFO`/`XInfo` allow-reply (switched to `parseChatMessage'`).
7. Signed send - `prepareMsgReqs` partitions recipients by `relayWebCapVersion` (binary-signed to v18+, sig-stripped JSON below); `memberBatch` does the single-connection equivalent for `sendGroupMemberMessages` and `sendPendingGroupMessages`.
## Open decisions
None outstanding.
Resolved: names finalized during implementation. Both key writes required (`groups.member_priv_key` and own-row `member_pub_key`). `XContact` includes the unsigned key at member creation and the signed allow-reply confirms it. Reuse `relayWebCapVersion` (18) as the binary floor. `user_member_key_sent` is a boolean, set `True` for key recipients in `sentTo`/`pending`/`forwarded`; no cap or error classification - `memberSendAction` skips dead connections (`Internal.hs:2608`), so they are re-filtered, not re-sent, until ready. Key change on receipt - reject any change, immutable. Signed send (item 7) - partition by binary capability. Key distribution runs in all p2p groups including incognito. First send after generation is signed. Sign criteria unchanged. Handshake allow-reply signed, confirms the key at join.
+9 -5
View File
@@ -2639,7 +2639,8 @@ processChatCommand cxt nm = \case
APINewGroup userId incognito gProfile -> withUserId userId $ \user -> do
g <- asks random
memberId <- liftIO $ MemberId <$> encodedRandomBytes g 12
gInfo <- newGroup user incognito gProfile False memberId Nothing Nothing
(_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair g
gInfo <- newGroup user incognito gProfile False memberId (Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey}) Nothing
createNewGroupItems user gInfo
pure $ CRGroupCreated user gInfo
NewGroup incognito gProfile -> withUser $ \User {userId} ->
@@ -3905,11 +3906,14 @@ processChatCommand cxt nm = \case
Just gInfo_' -> userProfileInGroup' user gInfo_' incognitoProfile
Nothing -> userProfileDirect user incognitoProfile Nothing True
dm <- case gInfo_ of
Just (Just gInfo) | useRelays' gInfo -> case relayMemberId_ of
Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend
Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId"
Just (Just gInfo)
| useRelays' gInfo -> case relayMemberId_ of
Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend
Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId"
| otherwise -> do
gInfo' <- ensureUserMemberKey gInfo
encodeConnInfoPQ pqSup chatV $ XContact profileToSend (groupMemberKey gInfo') (Just xContactId) welcomeSharedMsgId msg_
_ ->
-- TODO [member keys] send member key in groups
encodeConnInfoPQ pqSup chatV $ XContact profileToSend Nothing (Just xContactId) welcomeSharedMsgId msg_
subMode <- chatReadVar subscriptionMode
void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode
+94 -35
View File
@@ -34,13 +34,13 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Char (isDigit)
import Data.Containers.ListUtils (nubOrd)
import Data.Either (partitionEithers, rights)
import Data.Either (isRight, partitionEithers, rights)
import Data.Fixed (div')
import Data.Foldable (foldr')
import Data.Functor (($>))
import Data.Functor.Identity
import Data.Int (Int64)
import Data.List (find, foldl', mapAccumL, partition)
import Data.List (find, foldl', mapAccumL, nubBy, partition)
import Data.List.NonEmpty (NonEmpty (..), (<|))
import qualified Data.List.NonEmpty as L
import Data.Map.Strict (Map)
@@ -2225,15 +2225,41 @@ groupBindingData gks memberId memberKey = case gks >>= publicGroupKeys of
Just PublicGroupKeys {publicGroupId} -> smpEncode (publicGroupId, memberId)
Nothing -> smpEncode (memberId, memberKey)
ensureUserMemberKey :: GroupInfo -> CM GroupInfo
ensureUserMemberKey gInfo@GroupInfo {groupId, membership, groupKeys}
| useRelays' gInfo || isJust groupKeys = pure gInfo
| otherwise = do
g <- asks random
(_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair g
withStore' $ \db -> setUserMemberKey db groupId (groupMemberId' membership) memberPrivKey
pure gInfo {groupKeys = Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey}}
groupMemberKey :: GroupInfo -> Maybe MemberKey
groupMemberKey GroupInfo {groupKeys} = case groupKeys of
Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey} -> Just $ MemberKey $ C.publicKey memberPrivKey
_ -> Nothing
sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> GroupInfo -> Connection -> NonEmpty (ChatMsgEvent e) -> CM ()
sendGroupMemberMessages user gInfo@GroupInfo {groupId} conn events = do
when (connDisabled conn) $ throwChatError (CEConnectionDisabled conn)
let idsEvts = L.map (\evt -> (GroupId groupId, groupMsgSigning False gInfo evt, evt)) events
mode = if useRelays' gInfo then BMBinary else BMJson
(errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts
unless (null errs) $ toView $ CEvtChatErrors errs
forM_ (L.nonEmpty msgs) $ \msgs' ->
batchSendConnMessages mode user conn MsgFlags {notification = True} msgs'
forM_ (L.nonEmpty msgs) $ \msgs' -> do
let (mode, msgs'') = memberBatch gInfo conn msgs'
batchSendConnMessages mode user conn MsgFlags {notification = True} msgs''
dropSignature :: SndMessage -> SndMessage
dropSignature m = m {signedMsg_ = Nothing}
memberBatch :: GroupInfo -> Connection -> NonEmpty SndMessage -> (BatchMode, NonEmpty SndMessage)
memberBatch gInfo conn msgs
| useRelays' gInfo = (BMBinary, msgs)
| not anySigned = (BMJson, msgs)
| maxVersion (peerChatVRange conn) >= relayWebCapVersion = (BMBinary, msgs)
| otherwise = (BMJson, L.map dropSignature msgs)
where
anySigned = any (\SndMessage {signedMsg_} -> isJust signedMsg_) msgs
batchSendConnMessages :: BatchMode -> User -> Connection -> MsgFlags -> NonEmpty SndMessage -> CM ([Either ChatError SndMessage], Maybe PQEncryption)
batchSendConnMessages mode user conn msgFlags msgs =
@@ -2304,6 +2330,15 @@ encodeXMemberConnInfo GroupInfo {membership = GroupMember {memberId}, groupKeys}
in encodeSignedConnInfo signing xMemberEvt
Nothing -> throwChatError $ CEInternalError "no group keys for channel membership"
encodeSignedGroupConnInfo :: MsgEncodingI e => GroupInfo -> ChatMsgEvent e -> CM ByteString
encodeSignedGroupConnInfo GroupInfo {membership = GroupMember {memberId}, groupKeys} chatMsgEvent =
case groupKeys of
Just gks@GroupKeys {memberPrivKey} ->
let bindingData = groupBindingData (Just gks) memberId (C.publicKey memberPrivKey)
signing = MsgSigning CBGroup bindingData KRMember memberPrivKey
in encodeSignedConnInfo signing chatMsgEvent
Nothing -> throwChatError $ CEInternalError "no group keys for signed conn info"
deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
deliverMessage conn cmEventTag msgBody msgId = do
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
@@ -2456,22 +2491,22 @@ sendRelayCapIfNeeded user gInfo = do
sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> Bool -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
sendGroupMessages user gInfo scope asGroup members sign events = do
sendGroupProfileUpdate user gInfo scope asGroup members
sendGroupMessages_ user gInfo members sign events
gInfo' <- ensureUserMemberKey gInfo
sendGroupProfileUpdate user gInfo' scope asGroup members
sendGroupMessages_ user gInfo' members sign events
-- per-item signer variant of sendGroupMessages (used for per-item delete signing); preserves the profile-update prelude
sendGroupSignedMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> NonEmpty (Maybe MsgSigning, ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
sendGroupSignedMessages user gInfo scope asGroup members signedEvents = do
sendGroupProfileUpdate user gInfo scope asGroup members
sendGroupSignedMessages_ gInfo members signedEvents
gInfo' <- ensureUserMemberKey gInfo
sendGroupProfileUpdate user gInfo' scope asGroup members
sendGroupSignedMessages_ gInfo' members signedEvents
sendGroupProfileUpdate :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> CM ()
sendGroupProfileUpdate user gInfo scope asGroup members =
-- TODO [knocking] send current profile to pending member after approval?
when shouldSendProfileUpdate $
sendProfileUpdate `catchAllErrors` eToView
unless (null recipients) $ sendProfileAndKey `catchAllErrors` eToView
where
User {profile = p, userMemberProfileUpdatedAt} = user
User {userMemberProfileUpdatedAt} = user
GroupInfo {userMemberProfileSentAt} = gInfo
shouldSendProfileUpdate
| asGroup = False
@@ -2482,14 +2517,24 @@ sendGroupProfileUpdate user gInfo scope asGroup members =
(Just lastSentTs, Just lastUpdateTs) -> lastSentTs < lastUpdateTs
(Nothing, Just _) -> True
_ -> False
sendProfileUpdate = do
let members' = filter (`supportsVersion` memberProfileUpdateVersion) members
-- shouldSendProfileUpdate excludes incognito membership, so the badge is presented
profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile gInfo (membership gInfo) $ fromLocalProfile p
-- TODO [member keys] add key
void $ sendGroupMessage' user gInfo members' $ XInfo profileUpdate Nothing
currentTs <- liftIO getCurrentTime
withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs
ownKey = groupMemberKey gInfo
profileMembers
| shouldSendProfileUpdate = filter (`supportsVersion` memberProfileUpdateVersion) members
| otherwise = []
keyMembers
| isJust ownKey = filter (\m -> m `supportsVersion` groupMemberKeyVersion && not (userMemberKeySent m)) members
| otherwise = []
recipients = nubBy (\a b -> groupMemberId' a == groupMemberId' b) (profileMembers <> keyMembers)
sendProfileAndKey = do
let incognitoProfile = incognitoMembershipProfile gInfo
profile <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
(_, GroupSndResult {sentTo, pending, forwarded}) <- sendGroupMessages_ user gInfo recipients False (XInfo profile ownKey :| [])
when shouldSendProfileUpdate $ do
currentTs <- liftIO getCurrentTime
withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs
let delivered = [mId | (mId, _, r) <- sentTo, isRight r] <> [mId | (mId, _, _) <- pending] <> map groupMemberId' forwarded
keyDelivered = [groupMemberId' m | m <- keyMembers, groupMemberId' m `elem` delivered]
unless (null keyDelivered) $ withStore' $ \db -> setMembersMemberKeySent db keyDelivered
data GroupSndResult = GroupSndResult
{ sentTo :: [(GroupMemberId, Either ChatError [MessageId], Either ChatError ([Int64], PQEncryption))],
@@ -2545,17 +2590,27 @@ sendGroupSignedMessages_ gInfo@GroupInfo {groupId} recipientMembers signedEvents
mId = groupMemberId' m
mIds' = S.insert mId mIds
prepareMsgReqs :: MsgFlags -> NonEmpty (Either ChatError SndMessage) -> [(GroupMember, Connection)] -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq])
prepareMsgReqs msgFlags msgs toSendSeparate toSendBatched = do
let mode = if useRelays' gInfo then BMBinary else BMJson
batched_ = batchSndMessagesJSON mode msgs
case L.nonEmpty batched_ of
Just batched' -> do
let lenMsgs = length msgs
(memsSep, mreqsSep) = foldMembers lenMsgs sndMessageMBR msgs toSendSeparate
(memsBtch, mreqsBtch) = foldMembers (length batched' + lenMsgs) msgBatchMBR batched' toSendBatched
(memsSep <> memsBtch, mreqsSep <> mreqsBtch)
Nothing -> ([], [])
prepareMsgReqs msgFlags msgs toSendSeparate toSendBatched
| useRelays' gInfo = single BMBinary msgs toSendSeparate toSendBatched
| not anySigned = single BMJson msgs toSendSeparate toSendBatched
| otherwise =
let (sepBin, sepJson) = partition binaryCapable toSendSeparate
(btchBin, btchJson) = partition binaryCapable toSendBatched
(binIds, binReqs) = single BMBinary msgs sepBin btchBin
(jsonIds, jsonReqs) = single BMJson (L.map (fmap dropSignature) msgs) sepJson btchJson
in (binIds <> jsonIds, binReqs <> jsonReqs)
where
anySigned = any (either (const False) (\SndMessage {signedMsg_} -> isJust signedMsg_)) (L.toList msgs)
binaryCapable (m, _) = m `supportsVersion` relayWebCapVersion
single mode msgs' sep btch =
let batched_ = batchSndMessagesJSON mode msgs'
in case L.nonEmpty batched_ of
Just batched' ->
let lenMsgs = length msgs'
(memsSep, mreqsSep) = foldMembers lenMsgs sndMessageMBR msgs' sep
(memsBtch, mreqsBtch) = foldMembers (length batched' + lenMsgs) msgBatchMBR batched' btch
in (memsSep <> memsBtch, mreqsSep <> mreqsBtch)
Nothing -> ([], [])
foldMembers :: forall a. Int -> (Maybe Int -> Int -> a -> (ValueOrRef MsgBody, [MessageId])) -> NonEmpty (Either ChatError a) -> [(GroupMember, Connection)] -> ([GroupMemberId], [Either ChatError ChatMsgReq])
foldMembers lastRef mkMb mbs mems = snd $ foldr' foldMsgBodies (lastMemIdx_, ([], [])) mems
where
@@ -2673,10 +2728,10 @@ sendFwdMemberMessage member fwd verifiedMsg =
-- TODO ensure order - pending messages interleave with user input messages
sendPendingGroupMessages :: User -> GroupInfo -> GroupMember -> Connection -> CM ()
sendPendingGroupMessages user gInfo GroupMember {groupMemberId} conn = do
let mode = if useRelays' gInfo then BMBinary else BMJson
msgs <- withStore' $ \db -> getPendingGroupMessages db groupMemberId
forM_ (L.nonEmpty msgs) $ \msgs' -> do
void $ batchSendConnMessages mode user conn MsgFlags {notification = True} msgs'
let (mode, msgs'') = memberBatch gInfo conn msgs'
void $ batchSendConnMessages mode user conn MsgFlags {notification = True} msgs''
lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs'
saveDirectRcvMSG :: forall e. MsgEncodingI e => Connection -> MsgMeta -> ChatMessage e -> CM (Connection, RcvMessage)
@@ -2879,9 +2934,13 @@ joinAgentConnectionAsync cmdId updateConn connId enableNtfs cReqUri cInfo subMod
withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) updateConn connId enableNtfs cReqUri cInfo PQSupportOff subMode
allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> CM ()
allowAgentConnectionAsync user conn@Connection {connId, pqSupport, connChatVersion} confId msg = do
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn
allowAgentConnectionAsync user conn@Connection {pqSupport, connChatVersion} confId msg = do
dm <- encodeConnInfoPQ pqSupport connChatVersion msg
allowAgentConnectionInfo user conn confId dm
allowAgentConnectionInfo :: User -> Connection -> ConfirmationId -> ByteString -> CM ()
allowAgentConnectionInfo user conn@Connection {connId} confId dm = do
cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn
withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm
withStore' $ \db -> updateConnectionStatus db conn ConnAccepted
+40 -21
View File
@@ -832,11 +832,15 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
useRelays' gInfo == isJust rcvPG && pgId rcvPG == pgId curPG -> do
-- XGrpLinkInv here means we are connecting via prepared group, and we have to update user and host member records
(gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db cxt user gInfo m glInv
gInfoK <- ensureUserMemberKey gInfo'
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId)
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
-- TODO [member keys] send key
allowAgentConnectionAsync user conn' confId $ XInfo profileToSend Nothing
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfoK (fromLocalProfile <$> incognitoProfile)
if not (useRelays' gInfo) && maxVersion chatVRange >= relayWebCapVersion
then do
dm <- encodeSignedGroupConnInfo gInfoK $ XInfo profileToSend (groupMemberKey gInfoK)
allowAgentConnectionInfo user conn' confId dm
else allowAgentConnectionAsync user conn' confId $ XInfo profileToSend Nothing
toView $ CEvtGroupLinkConnecting user gInfo' m'
| otherwise -> messageError "x.grp.link.inv: publicGroupId mismatch"
XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do
@@ -856,7 +860,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
_ -> messageError "CONF from member must have x.grp.mem.info"
INFO _pqSupport connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
(signedMsg_, ChatMessage {chatVRange, chatMsgEvent}) <- parseChatMessage' conn connInfo
_conn' <- updatePeerChatVRange conn chatVRange
case chatMsgEvent of
XGrpMemInfo memId _memProfile
@@ -865,11 +869,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
pure ()
| otherwise -> messageError "x.grp.mem.info: memberId is different from expected"
-- sent when connecting via group link
XInfo _ _ ->
XInfo _ mKey
-- TODO Keep rejected member to allow them to appeal against rejection.
when (memberStatus m == GSMemRejected) $ do
deleteMemberConnection' m True
withStore' $ \db -> deleteGroupMember db user m
| memberStatus m == GSMemRejected -> do
deleteMemberConnection' m True
withStore' $ \db -> deleteGroupMember db user m
| otherwise -> storeMemberKey gInfo m mKey signedMsg_
XOk ->
-- transient relay-reject row cleanup after the rejection handshake completes
when (memberCategory m == GCHostMember && not (relayServesGroup gInfo)) $ do
@@ -972,9 +977,10 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
when (groupFeatureAllowed SGFHistory gInfo'' && not memberIsCustomer) $ sendHistory user gInfo'' m'
where
sendXGrpLinkMem gInfo'' = do
let incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo''
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile)
void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend) groupId
gInfo3 <- ensureUserMemberKey gInfo''
let incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo3
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo3 (fromIncognitoProfile <$> incognitoProfile)
sendGroupMemberMessages user gInfo3 conn (XGrpLinkMem profileToSend (groupMemberKey gInfo3) :| [])
_ -> do
unless (memberPending m) $ withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected
notifyMemberConnected gInfo m Nothing
@@ -1088,7 +1094,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
XFileCancel sharedMsgId -> xFileCancelGroup gInfo' (Just m'') sharedMsgId
XFileAcptInv sharedMsgId fileConnReq_ fName -> Nothing <$ xFileAcptInvGroup gInfo' m'' sharedMsgId fileConnReq_ fName
XInfo p mKey -> fmap ctx <$> xInfoMember gInfo' m'' p mKey msg brokerTs
XGrpLinkMem p -> Nothing <$ xGrpLinkMem gInfo' m'' conn' p
XGrpLinkMem p mKey -> Nothing <$ xGrpLinkMem gInfo' m'' conn' p mKey msg
XGrpLinkAcpt acceptance role memberId -> Nothing <$ xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs
XGrpRelayNew rl -> fmap ctx <$> xGrpRelayNew gInfo' m'' rl
XGrpRelayCap relayCap
@@ -1399,9 +1405,9 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
REQ invId pqSupport _ connInfo -> do
(signedMsg_, ChatMessage {chatVRange, chatMsgEvent}) <- parseChatMessage' conn connInfo
case chatMsgEvent of
XContact p _ xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ pqSupport
XContact p memberKey_ xContactId_ welcomeMsgId_ requestMsg_ -> profileContactRequest invId chatVRange p memberKey_ xContactId_ welcomeMsgId_ requestMsg_ pqSupport
XMember p joiningMemberId joiningMemberKey viaRelay -> memberJoinRequestViaRelay invId chatVRange signedMsg_ p joiningMemberId joiningMemberKey viaRelay
XInfo p _ -> profileContactRequest invId chatVRange p Nothing Nothing Nothing pqSupport
XInfo p _ -> profileContactRequest invId chatVRange p Nothing Nothing Nothing Nothing pqSupport
XGrpRelayInv groupRelayInv -> xGrpRelayInv invId chatVRange groupRelayInv
XGrpRelayTest challenge _ -> xGrpRelayTest invId chatVRange challenge
-- TODO show/log error, other events in contact request
@@ -1467,8 +1473,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
-- TODO add debugging output
_ -> pure ()
where
profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> CM ()
profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ welcomeMsgId_ requestMsg_ reqPQSup = do
profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe MemberKey -> Maybe XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> CM ()
profileContactRequest invId chatVRange p@Profile {displayName} memberKey_ xContactId_ welcomeMsgId_ requestMsg_ reqPQSup = do
(ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId
let v = maxVersion chatVRange
case gLinkInfo_ of
@@ -1624,7 +1630,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
messageError "processContactConnMessage: chat version range incompatible for accepting group join request"
| otherwise -> do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode Nothing Nothing
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode memberKey_ Nothing
(gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem'
@@ -2753,16 +2759,29 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
Profile {displayName = n', fullName = fn', shortDescr = sd', image = i', contactLink = cl'} = p'
xInfoMember :: GroupInfo -> GroupMember -> Profile -> Maybe MemberKey -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xInfoMember gInfo m p' mKey msg brokerTs = do
-- TODO [member keys] udpate key if it was Nothing and is set, prohibit key changes unless message is signed with the current key
xInfoMember gInfo m p' mKey msg@RcvMessage {signedMsg_} brokerTs = do
storeMemberKey gInfo m mKey signedMsg_
void $ processMemberProfileUpdate gInfo m p' (Just (msg, brokerTs))
pure $ memberEventDeliveryScope m
xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> CM ()
xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' = do
storeMemberKey :: GroupInfo -> GroupMember -> Maybe MemberKey -> Maybe SignedMsg -> CM ()
storeMemberKey gInfo GroupMember {groupMemberId, memberPubKey, memberId} mKey signedMsg_ =
forM_ mKey $ \(MemberKey k) -> case memberPubKey of
Just k0 -> when (k /= k0) $ messageError "member key change rejected, keeping current key"
Nothing
| keyCertified k -> withStore' $ \db -> setMemberPubKey db groupMemberId k
| otherwise -> messageError "member key not signed by that key, ignored"
where
keyCertified k = case signedMsg_ of
Just SignedMsg {chatBinding = CBGroup, signatures, signedBody} -> verifyGroupSig k (groupKeys gInfo) memberId signatures signedBody
_ -> False
xGrpLinkMem :: GroupInfo -> GroupMember -> Connection -> Profile -> Maybe MemberKey -> RcvMessage -> CM ()
xGrpLinkMem gInfo@GroupInfo {membership, businessChat} m@GroupMember {groupMemberId, memberCategory} Connection {viaGroupLink} p' mKey RcvMessage {signedMsg_} = do
xGrpLinkMemReceived <- withStore $ \db -> getXGrpLinkMemReceived db groupMemberId
if (viaGroupLink || isJust businessChat) && isNothing (memberContactId m) && memberCategory == GCHostMember && not xGrpLinkMemReceived
then do
storeMemberKey gInfo m mKey signedMsg_
m' <- processMemberProfileUpdate gInfo m p' Nothing
withStore' $ \db -> setXGrpLinkMemReceived db groupMemberId True
let connectedIncognito = memberIncognito membership
+11 -5
View File
@@ -86,12 +86,13 @@ import Simplex.Messaging.Version hiding (version)
-- 17 - allow host voice messages during member approval regardless of group voice setting (2026-02-10)
-- 18 - relay web capabilities (2026-05-31)
-- 19 - group roster (2026-06-18)
-- 20 - p2p group member keys for signing (2026-07-26)
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
-- This indirection is needed for backward/forward compatibility testing.
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
currentChatVersion :: VersionChat
currentChatVersion = VersionChat 19
currentChatVersion = VersionChat 20
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
supportedChatVRange :: VersionRangeChat
@@ -167,6 +168,10 @@ relayWebCapVersion = VersionChat 18
groupRosterVersion :: VersionChat
groupRosterVersion = VersionChat 19
-- members sign messages in p2p groups; member keys are distributed for verification
groupMemberKeyVersion :: VersionChat
groupMemberKeyVersion = VersionChat 20
agentToChatVersion :: VersionSMPA -> VersionChat
agentToChatVersion v
| v < pqdrSMPAgentVersion = initialChatVersion
@@ -500,7 +505,7 @@ data ChatMsgEvent (e :: MsgEncoding) where
XGrpAcpt :: MemberId -> ChatMsgEvent 'Json
XGrpLinkInv :: GroupLinkInvitation -> ChatMsgEvent 'Json
XGrpLinkReject :: GroupLinkRejection -> ChatMsgEvent 'Json
XGrpLinkMem :: Profile -> ChatMsgEvent 'Json
XGrpLinkMem :: Profile -> Maybe MemberKey -> ChatMsgEvent 'Json
XGrpLinkAcpt :: GroupAcceptance -> GroupMemberRole -> MemberId -> ChatMsgEvent 'Json
XGrpRelayInv :: GroupRelayInvitation -> ChatMsgEvent 'Json
XGrpRelayAcpt :: ShortLinkContact -> RelayCapabilities -> ChatMsgEvent 'Json
@@ -1269,7 +1274,7 @@ toCMEventTag msg = case msg of
XGrpAcpt _ -> XGrpAcpt_
XGrpLinkInv _ -> XGrpLinkInv_
XGrpLinkReject _ -> XGrpLinkReject_
XGrpLinkMem _ -> XGrpLinkMem_
XGrpLinkMem {} -> XGrpLinkMem_
XGrpLinkAcpt {} -> XGrpLinkAcpt_
XGrpRelayInv _ -> XGrpRelayInv_
XGrpRelayAcpt {} -> XGrpRelayAcpt_
@@ -1355,6 +1360,7 @@ requiresSignature = \case
XGrpRelayNew_ -> True
XGrpRoster_ -> True
XInfo_ -> True
XGrpLinkMem_ -> True
_ -> False
-- | Content events a member may sign (XMsgNew opt-in; XMsgUpdate/XMsgDel when the target was signed).
@@ -1437,7 +1443,7 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
XGrpLinkInv_ -> XGrpLinkInv <$> p "groupLinkInvitation"
XGrpLinkReject_ -> XGrpLinkReject <$> p "groupLinkRejection"
XGrpLinkMem_ -> XGrpLinkMem <$> p "profile"
XGrpLinkMem_ -> XGrpLinkMem <$> p "profile" <*> opt "memberKey"
XGrpLinkAcpt_ -> XGrpLinkAcpt <$> p "acceptance" <*> p "role" <*> p "memberId"
XGrpRelayInv_ -> XGrpRelayInv <$> p "groupRelayInvitation"
XGrpRelayAcpt_ -> XGrpRelayAcpt <$> p "relayLink" <*> (fromMaybe defaultRelayCapabilities <$> opt "relayCap")
@@ -1513,7 +1519,7 @@ chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case en
XGrpAcpt memId -> o ["memberId" .= memId]
XGrpLinkInv groupLinkInv -> o ["groupLinkInvitation" .= groupLinkInv]
XGrpLinkReject groupLinkRjct -> o ["groupLinkRejection" .= groupLinkRjct]
XGrpLinkMem profile -> o ["profile" .= profile]
XGrpLinkMem profile memberKey -> o $ ("memberKey" .=? memberKey) ["profile" .= profile]
XGrpLinkAcpt acceptance role memberId -> o ["acceptance" .= acceptance, "role" .= role, "memberId" .= memberId]
XGrpRelayInv groupRelayInv -> o ["groupRelayInvitation" .= groupRelayInv]
XGrpRelayAcpt relayLink relayCap -> o ["relayLink" .= relayLink, "relayCap" .= relayCap]
+2 -2
View File
@@ -159,13 +159,13 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do
pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.user_member_key_sent, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at,
-- from GroupMember
m.group_member_id, m.group_id, m.index_in_group, m.member_id, m.peer_chat_min_version, m.peer_chat_max_version, m.member_role, m.member_category, m.member_status, m.show_messages, m.member_restriction,
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.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.user_member_key_sent, m.relay_link, m.member_security_code, m.member_security_code_verified_at
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
+26 -3
View File
@@ -107,6 +107,9 @@ module Simplex.Chat.Store.Groups
deleteRosterTransfer,
deleteGroupRosterTransfers,
setGroupMemberKeyRole,
setUserMemberKey,
setMemberPubKey,
setMembersMemberKeySent,
setGroupMemberVerified,
createRelayForOwner,
getCreateRelayForMember,
@@ -260,11 +263,11 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..))
import Database.SQLite.Simple.QQ (sql)
#endif
type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
type MaybeGroupMemberRow = (Maybe GroupMemberId, Maybe GroupId, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId) :. ((Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow) :. (Maybe UTCTime, Maybe UTCTime) :. (Maybe UTCTime, Maybe Int64, Maybe Int64, Maybe Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe BoolInt, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
toMaybeGroupMember :: UTCTime -> Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, description, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) =
Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_))
toMaybeGroupMember now userContactId ((Just groupMemberId, Just groupId, Just indexInGroup, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId) :. ((Just profileId, Just displayName, Just fullName, shortDescr, description, image, contactLink, peerType, Just localAlias, contactPreferences) :. badgeRow :. domainRow) :. (Just createdAt, Just updatedAt) :. (supportChatTs, Just supportChatUnread, Just supportChatUnanswered, Just supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, Just userMemberKeySent, relayLink, memberCode_, memberCodeVerifiedAt_)) =
Just $ toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked') :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. ((profileId, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, contactPreferences) :. badgeRow :. domainRow) :. (createdAt, updatedAt) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, userMemberKeySent, relayLink, memberCode_, memberCodeVerifiedAt_))
toMaybeGroupMember _ _ _ = Nothing
createGroupLink :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO GroupLink
@@ -601,6 +604,7 @@ createContactMemberInv_ db User {userId, userContactId} groupId invitedByGroupMe
updatedAt = createdAt,
supportChat = Nothing,
memberPubKey,
userMemberKeySent = False,
relayLink = Nothing,
memberVerifiedCode = Nothing
}
@@ -1405,6 +1409,7 @@ createNewContactMember db gVar User {userId, userContactId} GroupInfo {groupId,
updatedAt = createdAt,
supportChat = Nothing,
memberPubKey = Nothing,
userMemberKeySent = False,
relayLink = Nothing,
memberVerifiedCode = Nothing
}
@@ -1690,6 +1695,23 @@ setGroupMemberKeyRole db GroupMember {groupMemberId} pubKey role = do
currentTs <- getCurrentTime
DB.execute db "UPDATE group_members SET member_pub_key = ?, member_role = ?, updated_at = ? WHERE group_member_id = ?" (pubKey, role, currentTs, groupMemberId)
setUserMemberKey :: DB.Connection -> GroupId -> GroupMemberId -> C.PrivateKeyEd25519 -> IO ()
setUserMemberKey db groupId membershipId memberPrivKey = do
currentTs <- getCurrentTime
DB.execute db "UPDATE groups SET member_priv_key = ?, updated_at = ? WHERE group_id = ?" (memberPrivKey, currentTs, groupId)
DB.execute db "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" (C.publicKey memberPrivKey, currentTs, membershipId)
setMemberPubKey :: DB.Connection -> GroupMemberId -> C.PublicKeyEd25519 -> IO ()
setMemberPubKey db groupMemberId pubKey = do
currentTs <- getCurrentTime
DB.execute db "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" (pubKey, currentTs, groupMemberId)
setMembersMemberKeySent :: DB.Connection -> [GroupMemberId] -> IO ()
setMembersMemberKeySent db memberIds = do
currentTs <- getCurrentTime
forM_ memberIds $ \memberId ->
DB.execute db "UPDATE group_members SET user_member_key_sent = ?, updated_at = ? WHERE group_member_id = ?" (BI True, currentTs, memberId)
setGroupMemberVerified :: DB.Connection -> User -> GroupMemberId -> Maybe Text -> IO ()
setGroupMemberVerified db User {userId} groupMemberId code = do
updatedAt <- getCurrentTime
@@ -2529,6 +2551,7 @@ createNewMember_
updatedAt = createdAt,
supportChat = Nothing,
memberPubKey,
userMemberKeySent = False,
relayLink = Nothing,
memberVerifiedCode = Nothing
}
+4 -4
View File
@@ -724,7 +724,7 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe
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.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.user_member_key_sent, m.relay_link, m.member_security_code, m.member_security_code_verified_at
FROM group_members m
JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id)
LEFT JOIN contacts c ON m.contact_id = c.contact_id
@@ -3094,7 +3094,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
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.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.user_member_key_sent, m.relay_link, m.member_security_code, m.member_security_code_verified_at,
-- quoted ChatItem
ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent,
-- quoted GroupMember
@@ -3103,14 +3103,14 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do
rp.display_name, rp.full_name, rp.short_descr, rp.description, rp.image, rp.contact_link, rp.chat_peer_type, rp.local_alias, rp.preferences,
rp.badge_proof, rp.badge_pres_header, rp.badge_expiry, rp.badge_type, rp.badge_verified, rp.badge_extra, rp.badge_master_key, rp.badge_signature, rp.badge_key_idx, rp.contact_domain, rp.contact_domain_proof, rp.contact_domain_verified,
rm.created_at, rm.updated_at,
rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at,
rm.support_chat_ts, rm.support_chat_items_unread, rm.support_chat_items_member_attention, rm.support_chat_items_mentions, rm.support_chat_last_msg_from_member_ts, rm.member_pub_key, rm.user_member_key_sent, rm.relay_link, rm.member_security_code, rm.member_security_code_verified_at,
-- deleted by GroupMember
dbm.group_member_id, dbm.group_id, dbm.index_in_group, dbm.member_id, dbm.peer_chat_min_version, dbm.peer_chat_max_version, dbm.member_role, dbm.member_category,
dbm.member_status, dbm.show_messages, dbm.member_restriction, dbm.invited_by, dbm.invited_by_group_member_id, dbm.local_display_name, dbm.contact_id, dbm.contact_profile_id, dbp.contact_profile_id,
dbp.display_name, dbp.full_name, dbp.short_descr, dbp.description, dbp.image, dbp.contact_link, dbp.chat_peer_type, dbp.local_alias, dbp.preferences,
dbp.badge_proof, dbp.badge_pres_header, dbp.badge_expiry, dbp.badge_type, dbp.badge_verified, dbp.badge_extra, dbp.badge_master_key, dbp.badge_signature, dbp.badge_key_idx, dbp.contact_domain, dbp.contact_domain_proof, dbp.contact_domain_verified,
dbm.created_at, dbm.updated_at,
dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at
dbm.support_chat_ts, dbm.support_chat_items_unread, dbm.support_chat_items_member_attention, dbm.support_chat_items_mentions, dbm.support_chat_last_msg_from_member_ts, dbm.member_pub_key, dbm.user_member_key_sent, dbm.relay_link, dbm.member_security_code, dbm.member_security_code_verified_at
FROM chat_items i
LEFT JOIN files f ON f.chat_item_id = i.chat_item_id
LEFT JOIN group_members m ON m.group_member_id = i.group_member_id
@@ -45,6 +45,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260714_member_security_code
import Simplex.Chat.Store.Postgres.Migrations.M20260715_profile_description
import Simplex.Chat.Store.Postgres.Migrations.M20260716_signed_history
import Simplex.Chat.Store.Postgres.Migrations.M20260720_server_roles
import Simplex.Chat.Store.Postgres.Migrations.M20260727_member_key_sent
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -89,7 +90,8 @@ schemaMigrations =
("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code),
("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description),
("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history),
("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles)
("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles),
("20260727_member_key_sent", m20260727_member_key_sent, Just down_m20260727_member_key_sent)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,19 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260727_member_key_sent where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260727_member_key_sent :: Text
m20260727_member_key_sent =
[r|
ALTER TABLE group_members ADD COLUMN user_member_key_sent SMALLINT NOT NULL DEFAULT 0;
|]
down_m20260727_member_key_sent :: Text
down_m20260727_member_key_sent =
[r|
ALTER TABLE group_members DROP COLUMN user_member_key_sent;
|]
@@ -843,7 +843,8 @@ CREATE TABLE test_chat_schema.group_members (
removed_at timestamp with time zone,
roster_served_version bigint,
member_security_code text,
member_security_code_verified_at timestamp with time zone
member_security_code_verified_at timestamp with time zone,
user_member_key_sent smallint DEFAULT 0 NOT NULL
);
+3 -1
View File
@@ -168,6 +168,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260714_member_security_code
import Simplex.Chat.Store.SQLite.Migrations.M20260715_profile_description
import Simplex.Chat.Store.SQLite.Migrations.M20260716_signed_history
import Simplex.Chat.Store.SQLite.Migrations.M20260720_server_roles
import Simplex.Chat.Store.SQLite.Migrations.M20260727_member_key_sent
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -335,7 +336,8 @@ schemaMigrations =
("20260714_member_security_code", m20260714_member_security_code, Just down_m20260714_member_security_code),
("20260715_profile_description", m20260715_profile_description, Just down_m20260715_profile_description),
("20260716_signed_history", m20260716_signed_history, Just down_m20260716_signed_history),
("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles)
("20260720_server_roles", m20260720_server_roles, Just down_m20260720_server_roles),
("20260727_member_key_sent", m20260727_member_key_sent, Just down_m20260727_member_key_sent)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,18 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260727_member_key_sent where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260727_member_key_sent :: Query
m20260727_member_key_sent =
[sql|
ALTER TABLE group_members ADD COLUMN user_member_key_sent INTEGER NOT NULL DEFAULT 0;
|]
down_m20260727_member_key_sent :: Query
down_m20260727_member_key_sent =
[sql|
ALTER TABLE group_members DROP COLUMN user_member_key_sent;
|]
@@ -255,6 +255,7 @@ CREATE TABLE group_members(
roster_served_version INTEGER,
member_security_code TEXT,
member_security_code_verified_at TEXT,
user_member_key_sent INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
+4 -4
View File
@@ -687,7 +687,7 @@ type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe
type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof)
type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId) :. ProfileRow :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime, Maybe C.PublicKeyEd25519, BoolInt, Maybe ShortLinkContact, Maybe Text, Maybe UTCTime)
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow
@@ -739,7 +739,7 @@ toGroupKeys publicGroupId_ (rootPrivKey, rootPubKey, memberPrivKey) =
in GroupKeys <$> publicGroupKeys <*> memberPrivKey
toGroupMember :: UTCTime -> Int64 -> GroupMemberRow -> GroupMember
toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, relayLink, memberCode_, memberCodeVerifiedAt_)) =
toGroupMember now userContactId ((groupMemberId, groupId, indexInGroup, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId) :. profileRow :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs, memberPubKey, BI userMemberKeySent, relayLink, memberCode_, memberCodeVerifiedAt_)) =
let memberProfile = rowToLocalProfile now profileRow
memberSettings = GroupMemberSettings {showMessages}
blockedByAdmin = maybe False mrsBlocked memberRestriction_
@@ -768,7 +768,7 @@ groupMemberQuery =
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.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.user_member_key_sent, m.relay_link, m.member_security_code, m.member_security_code_verified_at,
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,
@@ -813,7 +813,7 @@ groupInfoQueryFields =
pu.display_name, pu.full_name, pu.short_descr, pu.description, pu.image, pu.contact_link, pu.chat_peer_type, pu.local_alias, pu.preferences,
pu.badge_proof, pu.badge_pres_header, pu.badge_expiry, pu.badge_type, pu.badge_verified, pu.badge_extra, pu.badge_master_key, pu.badge_signature, pu.badge_key_idx, pu.contact_domain, pu.contact_domain_proof, pu.contact_domain_verified,
mu.created_at, mu.updated_at,
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at
mu.support_chat_ts, mu.support_chat_items_unread, mu.support_chat_items_member_attention, mu.support_chat_items_mentions, mu.support_chat_last_msg_from_member_ts, mu.member_pub_key, mu.user_member_key_sent, mu.relay_link, mu.member_security_code, mu.member_security_code_verified_at
|]
groupInfoQueryFrom :: Query
+1
View File
@@ -1146,6 +1146,7 @@ data GroupMember = GroupMember
updatedAt :: UTCTime,
supportChat :: Maybe GroupSupportChat,
memberPubKey :: Maybe C.PublicKeyEd25519,
userMemberKeySent :: Bool,
relayLink :: Maybe ShortLinkContact,
-- out-of-band verified security code for connectionless (channel) members;
-- regular members carry it in activeConn instead (see memberSecurityCode)