mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-12 16:09:00 +00:00
core: public groups - roster of privileged members (#7017)
This commit is contained in:
@@ -67,6 +67,9 @@ module Simplex.Chat.Store.Groups
|
||||
getGroupMembersByIndexes,
|
||||
getSupportScopeMembersByIndexes,
|
||||
getGroupModerators,
|
||||
getGroupRosterMembers,
|
||||
getGroupAdminsMods,
|
||||
getGroupOnlyMembers,
|
||||
getGroupOwners,
|
||||
getGroupRelayMembers,
|
||||
getGroupMembersForExpiration,
|
||||
@@ -84,7 +87,19 @@ module Simplex.Chat.Store.Groups
|
||||
getGroupRelayById,
|
||||
getGroupRelayByGMId,
|
||||
getGroupRelays,
|
||||
getConnectedGroupRelays,
|
||||
getPublishableGroupRelays,
|
||||
setGroupRosterVersion,
|
||||
getGroupRosterVersion,
|
||||
getGroupRoster,
|
||||
RcvRosterTransfer (..),
|
||||
createRosterTransfer,
|
||||
getRosterTransferVersion,
|
||||
getRosterTransferId,
|
||||
getRosterTransfer,
|
||||
setGroupLiveRoster,
|
||||
deleteRosterTransfer,
|
||||
deleteGroupRosterTransfers,
|
||||
setGroupMemberKeyRole,
|
||||
createRelayForOwner,
|
||||
getCreateRelayForMember,
|
||||
createRelayConnection,
|
||||
@@ -173,6 +188,7 @@ module Simplex.Chat.Store.Groups
|
||||
createLinkOwnerMember,
|
||||
updatePreparedChannelMember,
|
||||
updateUnknownMemberAnnounced,
|
||||
updateRosterMemberAnnounced,
|
||||
updateUserMemberProfileSentAt,
|
||||
setGroupCustomData,
|
||||
setGroupUIThemes,
|
||||
@@ -216,6 +232,8 @@ import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Types.UITheme
|
||||
import Simplex.Messaging.Agent.Protocol (ConfirmationId, ConnId, CreatedConnLink (..), InvitationId, OwnerAuth (..), UserId)
|
||||
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow)
|
||||
import qualified Simplex.FileTransfer.Description as FD
|
||||
import Simplex.Messaging.Encoding (smpDecode, smpEncode)
|
||||
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
|
||||
import Simplex.Messaging.Agent.Store.Entity (DBEntityId)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
@@ -359,6 +377,7 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays
|
||||
Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId)
|
||||
Nothing -> (Nothing, Nothing, Nothing)
|
||||
fullGroupPreferences = mergeGroupPreferences groupPreferences
|
||||
rosterVersion0 = if useRelays then Just (VersionRoster 0) else Nothing
|
||||
currentTs <- getCurrentTime
|
||||
customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile
|
||||
withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
|
||||
@@ -389,11 +408,11 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays
|
||||
INSERT INTO groups
|
||||
(use_relays, creating_in_progress, local_display_name, user_id, group_profile_id, enable_ntfs,
|
||||
created_at, updated_at, chat_ts, user_member_profile_sent_at,
|
||||
root_priv_key, root_pub_key, member_priv_key, public_member_count)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
root_priv_key, root_pub_key, member_priv_key, public_member_count, roster_version)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (BI useRelays, BI useRelays, ldn, userId, profileId, BI True, currentTs, currentTs, currentTs, currentTs)
|
||||
:. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_)
|
||||
:. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_, rosterVersion0)
|
||||
)
|
||||
insertedRowId db
|
||||
let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys
|
||||
@@ -420,6 +439,7 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays
|
||||
chatItemTTL = Nothing,
|
||||
uiThemes = Nothing,
|
||||
groupSummary = GroupSummary {currentMembers = 1, publicMemberCount = publicMemberCount_},
|
||||
rosterVersion = rosterVersion0,
|
||||
customData = Nothing,
|
||||
membersRequireAttention = 0,
|
||||
viaGroupLinkUri = Nothing,
|
||||
@@ -497,6 +517,7 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti
|
||||
chatItemTTL = Nothing,
|
||||
uiThemes = Nothing,
|
||||
groupSummary = GroupSummary {currentMembers = 2, publicMemberCount = Nothing},
|
||||
rosterVersion = Nothing,
|
||||
customData = Nothing,
|
||||
membersRequireAttention = 0,
|
||||
viaGroupLinkUri = Nothing,
|
||||
@@ -1215,10 +1236,42 @@ getGroupModerators db cxt user@User {userId, userContactId} GroupInfo {groupId}
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)")
|
||||
(userId, groupId, userContactId, GRModerator, GRAdmin, GROwner)
|
||||
|
||||
-- The full roster set - members, moderators and admins - excluding owners (link-anchored) and
|
||||
-- left/removed members. For the privileged subset only use getGroupAdminsMods; for plain members
|
||||
-- only use getGroupOnlyMembers.
|
||||
getGroupRosterMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupRosterMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
currentTs <- getCurrentTime
|
||||
filter memberCurrent . map (toContactMember currentTs cxt user)
|
||||
<$> DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)")
|
||||
(userId, groupId, userContactId, GRMember, GRModerator, GRAdmin)
|
||||
|
||||
-- Moderators and admins only (excluding owners and plain members) - the set introduced to a
|
||||
-- joiner; plain members are learned from the roster blob, not via introductions.
|
||||
getGroupAdminsMods :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupAdminsMods db cxt user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
currentTs <- getCurrentTime
|
||||
filter memberCurrent . map (toContactMember currentTs cxt user)
|
||||
<$> DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?)")
|
||||
(userId, groupId, userContactId, GRModerator, GRAdmin)
|
||||
|
||||
getGroupOnlyMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupOnlyMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
currentTs <- getCurrentTime
|
||||
filter memberCurrent . map (toContactMember currentTs cxt user)
|
||||
<$> DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ?")
|
||||
(userId, groupId, userContactId, GRMember)
|
||||
|
||||
getGroupOwners :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember]
|
||||
getGroupOwners db cxt user@User {userId, userContactId} GroupInfo {groupId} = do
|
||||
ts <- getCurrentTime
|
||||
map (toContactMember ts cxt user)
|
||||
currentTs <- getCurrentTime
|
||||
filter memberCurrent . map (toContactMember currentTs cxt user)
|
||||
<$> DB.query
|
||||
db
|
||||
(groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role = ?")
|
||||
@@ -1373,21 +1426,30 @@ getGroupRelays db GroupInfo {groupId} =
|
||||
(groupRelayQuery <> " WHERE gr.group_id = ?")
|
||||
(Only groupId)
|
||||
|
||||
getConnectedGroupRelays :: DB.Connection -> GroupInfo -> IO [GroupRelay]
|
||||
getConnectedGroupRelays db GroupInfo {groupId} =
|
||||
map toGroupRelay
|
||||
<$> DB.query
|
||||
db
|
||||
( groupRelayQuery
|
||||
<> " "
|
||||
<> [sql|
|
||||
JOIN group_members m ON m.group_member_id = gr.group_member_id
|
||||
WHERE gr.group_id = ?
|
||||
AND m.member_status = ?
|
||||
AND gr.relay_status IN (?,?)
|
||||
|]
|
||||
)
|
||||
(groupId, GSMemConnected, RSAccepted, RSActive)
|
||||
-- Relays whose link is published to subscribers: acked relays (RSAcknowledgedRoster/RSActive) plus
|
||||
-- pre-roster relays at RSAccepted (below groupRosterVersion, they can't ack a roster), gated by the
|
||||
-- relay's negotiated version read from its member connection.
|
||||
getPublishableGroupRelays :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupRelay]
|
||||
getPublishableGroupRelays db cxt user gInfo@GroupInfo {groupId} = do
|
||||
relays <-
|
||||
map toGroupRelay
|
||||
<$> DB.query
|
||||
db
|
||||
( groupRelayQuery
|
||||
<> " "
|
||||
<> [sql|
|
||||
JOIN group_members m ON m.group_member_id = gr.group_member_id
|
||||
WHERE gr.group_id = ?
|
||||
AND m.member_status = ?
|
||||
AND gr.relay_status IN (?,?,?)
|
||||
|]
|
||||
)
|
||||
(groupId, GSMemConnected, RSAccepted, RSAcknowledgedRoster, RSActive)
|
||||
members <- getGroupRelayMembers db cxt user gInfo
|
||||
pure [gr | gr@GroupRelay {groupMemberId} <- relays, m <- members, groupMemberId' m == groupMemberId, publishable gr m]
|
||||
where
|
||||
publishable GroupRelay {relayStatus} m =
|
||||
relayStatus /= RSAccepted || not (m `supportsVersion` groupRosterVersion)
|
||||
|
||||
groupRelayQuery :: Query
|
||||
groupRelayQuery =
|
||||
@@ -1405,6 +1467,149 @@ toGroupRelay ((groupRelayId, groupMemberId, chatRelayId, address, displayName, f
|
||||
relayCap = RelayCapabilities {webDomain}
|
||||
in GroupRelay {groupRelayId, groupMemberId, userChatRelay, relayStatus, relayLink, relayCap}
|
||||
|
||||
setGroupRosterVersion :: DB.Connection -> GroupInfo -> VersionRoster -> IO ()
|
||||
setGroupRosterVersion db GroupInfo {groupId} v = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "UPDATE groups SET roster_version = ?, updated_at = ? WHERE group_id = ?" (v, currentTs, groupId)
|
||||
|
||||
-- Persisted roster version (the gate baseline; the in-memory gInfo copy is batch-constant and stale on reorder).
|
||||
getGroupRosterVersion :: DB.Connection -> GroupInfo -> IO (Maybe VersionRoster)
|
||||
getGroupRosterVersion db GroupInfo {groupId} =
|
||||
fmap join . maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT roster_version FROM groups WHERE group_id = ?" (Only groupId)
|
||||
|
||||
-- The live roster header a relay re-serves to joiners, with the completed blob served alongside it
|
||||
-- (both are written together at completion, so the blob is present whenever the header is).
|
||||
getGroupRoster :: DB.Connection -> GroupInfo -> IO (Maybe (GroupMemberId, UTCTime, SignedMsg, Maybe ByteString))
|
||||
getGroupRoster db GroupInfo {groupId} =
|
||||
(>>= toRoster)
|
||||
<$> maybeFirstRow
|
||||
id
|
||||
( DB.query
|
||||
db
|
||||
"SELECT roster_sending_owner_gm_id, roster_broker_ts, roster_msg_chat_binding, roster_msg_signatures, roster_msg_body, roster_blob FROM groups WHERE group_id = ?"
|
||||
(Only groupId)
|
||||
)
|
||||
where
|
||||
toRoster (Just ownerGMId, Just brokerTs, Just cb, Just (Binary sigsBs), Just (Binary body), blob_) =
|
||||
(\sigs -> (ownerGMId, brokerTs, SignedMsg cb sigs body, (\(Binary b) -> b) <$> blob_)) <$> eitherToMaybe (smpDecode sigsBs)
|
||||
toRoster _ = Nothing
|
||||
|
||||
-- A per-source in-flight roster transfer, keyed (group_id, from_member_id): replaces the single
|
||||
-- roster_pending_* slot, so two relays serving one member can't share a chunk stream. The signed-header
|
||||
-- columns are relay-only (NULL on members), promoted to the live roster_msg_* on groups at completion.
|
||||
createRosterTransfer :: DB.Connection -> GroupInfo -> GroupMemberId -> VersionRoster -> FD.FileDigest -> GroupMemberId -> UTCTime -> Maybe SignedMsg -> IO Int64
|
||||
createRosterTransfer db GroupInfo {groupId} fromMemberId v digest ownerGMId brokerTs sm_ = do
|
||||
-- one in-flight transfer per (group, source): drop any prior row from this source so the INSERT can't hit
|
||||
-- the UNIQUE constraint even if the caller's fs/handle cleanup was skipped (the scratch file would then leak
|
||||
-- until group delete, but the transfer never gets stuck). Normally cleanupRosterTransfer ran first.
|
||||
DB.execute db "DELETE FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId)
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO rcv_roster_transfers
|
||||
(group_id, from_member_id, roster_version, roster_digest, sending_owner_gm_id, broker_ts,
|
||||
roster_msg_chat_binding, roster_msg_signatures, roster_msg_body)
|
||||
VALUES (?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
( (groupId, fromMemberId, v, Binary (FD.unFileDigest digest), ownerGMId, brokerTs)
|
||||
:. ((\SignedMsg {chatBinding} -> chatBinding) <$> sm_, (\SignedMsg {signatures} -> Binary (smpEncode signatures)) <$> sm_, (\SignedMsg {signedBody} -> Binary signedBody) <$> sm_)
|
||||
)
|
||||
insertedRowId db
|
||||
|
||||
getRosterTransferVersion :: DB.Connection -> GroupInfo -> GroupMemberId -> IO (Maybe VersionRoster)
|
||||
getRosterTransferVersion db GroupInfo {groupId} fromMemberId =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT roster_version FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId)
|
||||
|
||||
getRosterTransferId :: DB.Connection -> GroupInfo -> GroupMemberId -> IO (Maybe Int64)
|
||||
getRosterTransferId db GroupInfo {groupId} fromMemberId =
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT roster_transfer_id FROM rcv_roster_transfers WHERE group_id = ? AND from_member_id = ?" (groupId, fromMemberId)
|
||||
|
||||
-- An in-flight received roster transfer (a rcv_roster_transfers row joined to its scratch file), read at
|
||||
-- completion. The header is the relay's re-serve SignedMsg -- present only on a serving relay (NULL on a
|
||||
-- member, whose live roster_msg_* stay NULL so it never re-serves).
|
||||
data RcvRosterTransfer = RcvRosterTransfer
|
||||
{ rosterTransferId :: Int64,
|
||||
rosterTransferVersion :: VersionRoster,
|
||||
rosterTransferDigest :: FD.FileDigest,
|
||||
rosterTransferOwnerGMId :: GroupMemberId,
|
||||
rosterTransferBrokerTs :: UTCTime,
|
||||
rosterTransferHeader :: Maybe SignedMsg
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
-- The in-flight transfer for a received roster file (joined via files.roster_transfer_id), with its
|
||||
-- relay-only signed header. Read at completion to apply, promote into the live roster, and ack.
|
||||
getRosterTransfer :: DB.Connection -> Int64 -> IO (Maybe RcvRosterTransfer)
|
||||
getRosterTransfer db fileId =
|
||||
(>>= toTransfer)
|
||||
<$> maybeFirstRow
|
||||
id
|
||||
( DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT t.roster_transfer_id, t.roster_version, t.roster_digest, t.sending_owner_gm_id, t.broker_ts,
|
||||
t.roster_msg_chat_binding, t.roster_msg_signatures, t.roster_msg_body
|
||||
FROM rcv_roster_transfers t
|
||||
JOIN files f ON f.roster_transfer_id = t.roster_transfer_id
|
||||
WHERE f.file_id = ?
|
||||
|]
|
||||
(Only fileId)
|
||||
)
|
||||
where
|
||||
toTransfer (tId, v, Binary d, ownerGMId, brokerTs, cb_, sigs_, body_) =
|
||||
Just
|
||||
RcvRosterTransfer
|
||||
{ rosterTransferId = tId,
|
||||
rosterTransferVersion = v,
|
||||
rosterTransferDigest = FD.FileDigest d,
|
||||
rosterTransferOwnerGMId = ownerGMId,
|
||||
rosterTransferBrokerTs = brokerTs,
|
||||
rosterTransferHeader = sm_
|
||||
}
|
||||
where
|
||||
sm_ = case (cb_, sigs_, body_) of
|
||||
(Just cb, Just (Binary sigsBs), Just (Binary body)) ->
|
||||
(\sigs -> SignedMsg cb sigs body) <$> eitherToMaybe (smpDecode sigsBs)
|
||||
_ -> Nothing
|
||||
|
||||
-- Write the single live roster on groups from a completed transfer's values (header NULL on a member,
|
||||
-- so its live roster_msg_* stay NULL and it never re-serves; only relays re-serve).
|
||||
setGroupLiveRoster :: DB.Connection -> GroupInfo -> VersionRoster -> GroupMemberId -> UTCTime -> Maybe SignedMsg -> ByteString -> IO ()
|
||||
setGroupLiveRoster db GroupInfo {groupId} v ownerGMId brokerTs sm_ blob = do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE groups SET
|
||||
roster_version = ?, roster_blob = ?,
|
||||
roster_sending_owner_gm_id = ?, roster_broker_ts = ?,
|
||||
roster_msg_chat_binding = ?, roster_msg_signatures = ?, roster_msg_body = ?,
|
||||
updated_at = ?
|
||||
WHERE group_id = ?
|
||||
|]
|
||||
( (v, Binary blob, ownerGMId, brokerTs)
|
||||
:. ((\SignedMsg {chatBinding} -> chatBinding) <$> sm_, (\SignedMsg {signatures} -> Binary (smpEncode signatures)) <$> sm_, (\SignedMsg {signedBody} -> Binary signedBody) <$> sm_, currentTs, groupId)
|
||||
)
|
||||
|
||||
-- Delete one in-flight transfer row (its files/rcv_files/rcv_file_chunks are removed separately, with
|
||||
-- the on-disk file). Caller removes the fs file + cached handle first.
|
||||
deleteRosterTransfer :: DB.Connection -> Int64 -> IO ()
|
||||
deleteRosterTransfer db transferId =
|
||||
DB.execute db "DELETE FROM rcv_roster_transfers WHERE roster_transfer_id = ?" (Only transferId)
|
||||
|
||||
-- All in-flight transfers for a group (group delete).
|
||||
deleteGroupRosterTransfers :: DB.Connection -> Int64 -> IO ()
|
||||
deleteGroupRosterTransfers db groupId =
|
||||
DB.execute db "DELETE FROM rcv_roster_transfers WHERE group_id = ?" (Only groupId)
|
||||
|
||||
setGroupMemberKeyRole :: DB.Connection -> GroupMember -> C.PublicKeyEd25519 -> GroupMemberRole -> IO ()
|
||||
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)
|
||||
|
||||
createRelayForOwner :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupMember
|
||||
createRelayForOwner db cxt gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {relayProfile = RelayProfile {displayName}} = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
@@ -1713,9 +1918,9 @@ getRelayServedGroups db cxt User {userId, userContactId} = do
|
||||
<$> DB.query
|
||||
db
|
||||
( groupInfoQuery
|
||||
<> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?)"
|
||||
<> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?, ?)"
|
||||
)
|
||||
(userId, userContactId, RSAccepted, RSActive)
|
||||
(userId, userContactId, RSAccepted, RSAcknowledgedRoster, RSActive)
|
||||
|
||||
getRelayPublishableGroups :: DB.Connection -> User -> IO [(Int64, B64UrlByteString, Maybe PublicGroupAccess)]
|
||||
getRelayPublishableGroups db User {userId, userContactId} =
|
||||
@@ -3182,11 +3387,11 @@ createLinkOwnerMember db cxt user@User {userId, userContactId} GroupInfo {groupI
|
||||
where
|
||||
VersionRange minV maxV = vr cxt
|
||||
|
||||
-- member_pub_key is not updated here — introduced members are owners
|
||||
-- whose keys are loaded from link data (trusted out-of-band).
|
||||
-- Updating from an in-band message would allow a compromised relay to substitute keys.
|
||||
-- Intro refreshes only profile / status / peer version. Role and key stay owner-authoritative
|
||||
-- (the owner-signed roster for members/moderators/admins, link data for owners), so taking either from
|
||||
-- an in-band relayed intro would let a compromised relay substitute them.
|
||||
updatePreparedChannelMember :: DB.Connection -> StoreCxt -> User -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember
|
||||
updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do
|
||||
updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {v, profile} = do
|
||||
_ <- updateMemberProfile db cxt user member profile
|
||||
currentTs <- liftIO getCurrentTime
|
||||
liftIO $
|
||||
@@ -3194,14 +3399,13 @@ updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupM
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET member_role = ?,
|
||||
member_status = ?,
|
||||
SET member_status = ?,
|
||||
peer_chat_min_version = ?,
|
||||
peer_chat_max_version = ?,
|
||||
updated_at = ?
|
||||
WHERE user_id = ? AND group_member_id = ?
|
||||
|]
|
||||
(memberRole, GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId)
|
||||
(GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId)
|
||||
getGroupMemberById db cxt user groupMemberId
|
||||
where
|
||||
VersionRange minV maxV = maybe memberChatVRange fromChatVRange v
|
||||
@@ -3233,6 +3437,30 @@ updateUnknownMemberAnnounced db cxt user@User {userId} invitingMember unknownMem
|
||||
VersionRange minV maxV = maybe memberChatVRange fromChatVRange v
|
||||
memberPubKey_ = (\(MemberKey k) -> k) <$> memberKey
|
||||
|
||||
-- Like updateUnknownMemberAnnounced but preserves member_role and member_pub_key
|
||||
-- (roster-established for moderators/admins; the dissemination carries only the profile).
|
||||
updateRosterMemberAnnounced :: DB.Connection -> StoreCxt -> User -> GroupMember -> GroupMember -> MemberInfo -> GroupMemberStatus -> ExceptT StoreError IO GroupMember
|
||||
updateRosterMemberAnnounced db cxt user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {v, profile} status = do
|
||||
_ <- updateMemberProfile db cxt user unknownMember profile
|
||||
currentTs <- liftIO getCurrentTime
|
||||
liftIO $
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
UPDATE group_members
|
||||
SET member_category = ?,
|
||||
member_status = ?,
|
||||
invited_by_group_member_id = ?,
|
||||
peer_chat_min_version = ?,
|
||||
peer_chat_max_version = ?,
|
||||
updated_at = ?
|
||||
WHERE user_id = ? AND group_member_id = ?
|
||||
|]
|
||||
((GCPostMember, status, groupMemberId' invitingMember) :. (minV, maxV, currentTs, userId, groupMemberId))
|
||||
getGroupMemberById db cxt user groupMemberId
|
||||
where
|
||||
VersionRange minV maxV = maybe memberChatVRange fromChatVRange v
|
||||
|
||||
updateUserMemberProfileSentAt :: DB.Connection -> User -> GroupInfo -> UTCTime -> IO ()
|
||||
updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs =
|
||||
DB.execute
|
||||
|
||||
Reference in New Issue
Block a user