mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-17 03:45:01 +00:00
wip
This commit is contained in:
@@ -2739,7 +2739,7 @@ processChatCommand cxt nm = \case
|
||||
when (useRelays' gInfo && (isRosterRole newRole || anyPrivilegedTarget) && memberRole' (membership gInfo) /= GROwner) $
|
||||
throwCmdError "only the group owner can change moderator and admin roles"
|
||||
when (useRelays' gInfo && isRosterRole newRole && finalPrivilegedCount > maxGroupRosterSize) $
|
||||
throwCmdError $ "the number of moderators and admins would exceed the limit of " <> show maxGroupRosterSize
|
||||
throwCmdError $ "the number of members, moderators and admins would exceed the limit of " <> show maxGroupRosterSize
|
||||
(errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems
|
||||
(errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g currentMems
|
||||
unless (null acis) $ toView $ CEvtNewChatItems user acis
|
||||
@@ -2865,7 +2865,7 @@ processChatCommand cxt nm = \case
|
||||
when (memCount > 1 && anyAdmin) $ throwCmdError "can't remove multiple members when admins selected"
|
||||
assertUserGroupRole gInfo $ max GRAdmin maxRole
|
||||
when (useRelays' gInfo && anyPrivilegedRemoved && memberRole' (membership gInfo) /= GROwner) $
|
||||
throwCmdError "only the group owner can remove moderators and admins"
|
||||
throwCmdError "only the group owner can remove members, moderators and admins"
|
||||
(errs1, deleted1) <- deleteInvitedMems user invitedMems
|
||||
let recipients = filter memberCurrent members
|
||||
(errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing recipients currentMems
|
||||
|
||||
@@ -1239,7 +1239,7 @@ redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDes
|
||||
|
||||
-- Roles carried by the roster; owners are on the link, not the roster.
|
||||
isRosterRole :: GroupMemberRole -> Bool
|
||||
isRosterRole r = r == GRModerator || r == GRAdmin
|
||||
isRosterRole r = r == GRMember || r == GRModerator || r == GRAdmin
|
||||
|
||||
-- Drop non-privileged-role entries and de-duplicate by memberId, keeping the first.
|
||||
-- Runs on the parsed roster blob.
|
||||
@@ -1256,8 +1256,8 @@ validateGroupRoster entries =
|
||||
buildGroupRoster :: [GroupMember] -> [RosterMember]
|
||||
buildGroupRoster mods = mapMaybe rosterMember mods
|
||||
where
|
||||
rosterMember m@GroupMember {memberId, memberPubKey, memberRole}
|
||||
| isRosterRole memberRole = (\k -> RosterMember {memberId, name = memberShortenedName m, key = MemberKey k, role = memberRole}) <$> memberPubKey
|
||||
rosterMember GroupMember {memberId, memberPubKey, memberRole}
|
||||
| isRosterRole memberRole = (\k -> RosterMember {memberId, key = MemberKey k, role = memberRole, privileges = 0}) <$> memberPubKey
|
||||
| otherwise = Nothing
|
||||
|
||||
sendHistory :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
@@ -2212,7 +2212,7 @@ bumpAndBroadcastRoster user gInfo = do
|
||||
let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo)
|
||||
(relays, mods) <- withStore' $ \db -> do
|
||||
relays <- getGroupRelayMembers db cxt user gInfo
|
||||
mods <- getGroupRosterMembers db cxt user gInfo
|
||||
mods <- (++) <$> getGroupRosterMembers db cxt user gInfo <*> getGroupOnlyMembers db cxt user gInfo
|
||||
setGroupRosterVersion db gInfo rosterVer
|
||||
pure (relays, mods)
|
||||
forM_ (L.nonEmpty relays) $ \relays' ->
|
||||
@@ -2223,7 +2223,7 @@ sendGroupRosterToRelay :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
sendGroupRosterToRelay user gInfo relayMember =
|
||||
forM_ (rosterVersion gInfo) $ \rosterVer -> do
|
||||
cxt <- chatStoreCxt
|
||||
mods <- withStore' $ \db -> getGroupRosterMembers db cxt user gInfo
|
||||
mods <- withStore' $ \db -> (++) <$> getGroupRosterMembers db cxt user gInfo <*> getGroupOnlyMembers db cxt user gInfo
|
||||
sendRoster user gInfo [relayMember] rosterVer (buildGroupRoster mods)
|
||||
|
||||
-- Row-less send (no files/snd_files rows, so no send-side cleanup); redelivery is the agent's.
|
||||
|
||||
@@ -3209,7 +3209,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
| senderRole < GRAdmin || senderRole < fromRole =
|
||||
messageError "x.grp.mem.role with insufficient member permissions" $> Nothing
|
||||
| useRelays' gInfo && (isRosterRole memRole || isRosterRole fromRole) && senderRole /= GROwner =
|
||||
messageError "x.grp.mem.role: only the owner can change moderator/admin roles in relay groups" $> Nothing
|
||||
messageError "x.grp.mem.role: only the owner can change member, moderator and admin roles in relay groups" $> Nothing
|
||||
| otherwise = do
|
||||
withStore' $ \db -> updateGroupMemberRole db user member memRole
|
||||
(gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m
|
||||
@@ -3224,6 +3224,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
xGrpRoster gInfo author GroupRoster {version = newVer, fileInv = InlineFileInvitation {fileSize, fileDigest}} verifiedMsg sharedMsgId_ brokerTs
|
||||
-- only an owner may sign a roster; otherwise a relay could route it as a member whose key it controls
|
||||
| memberRole' author /= GROwner = messageError "x.grp.roster: not signed by an owner" $> Nothing
|
||||
| fileSize > maxGroupRosterBytes = messageError "x.grp.roster: roster blob size exceeds limit" $> Nothing
|
||||
| otherwise = case verifiedMsg of
|
||||
-- unreachable: XGrpRoster is in requiresSignature, so withVerifiedMsg rejected unsigned
|
||||
VMUnsigned _ -> pure Nothing
|
||||
@@ -3307,7 +3308,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
members <- withStore' $ \db -> getGroupMembers db cxt user gInfo
|
||||
serveRoster user gInfo (filter rosterRecipient members) `catchAllErrors` eToView
|
||||
where
|
||||
rosterRecipient m@GroupMember {activeConn} = memberCurrent m && isJust activeConn && not (isRelay m) && memberRole' m /= GROwner
|
||||
rosterRecipient m = memberCurrent m && not (isRelay m) && memberRole' m /= GROwner && isJust (readyMemberConn m)
|
||||
|
||||
-- TOFU apply: pin each member's key on first use, then update roles.
|
||||
processRosterEntries :: DB.Connection -> GroupInfo -> GroupMemberRole -> [RosterMember] -> ExceptT StoreError IO ([MemberId], [(GroupMember, GroupMemberRole)])
|
||||
@@ -3315,18 +3316,18 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
let rosterIds = map (\RosterMember {memberId} -> memberId) entries
|
||||
acc <- foldrM applyRosterEntry ([], []) entries
|
||||
-- absent privileged members revert to the joiner default
|
||||
currentPriv <- liftIO $ getGroupRosterMembers db cxt user gInfo
|
||||
currentPriv <- liftIO $ (++) <$> getGroupRosterMembers db cxt user gInfo <*> getGroupOnlyMembers db cxt user gInfo
|
||||
liftIO $ forM_ currentPriv $ \m ->
|
||||
when (memberId' m `notElem` rosterIds) $
|
||||
updateGroupMemberRole db user m defaultRole
|
||||
pure acc
|
||||
where
|
||||
-- entry-level failure (StoreError or IO exception) is muted; the entry is dropped
|
||||
applyRosterEntry RosterMember {memberId, name, key = MemberKey pubKey, role} (cs, as) =
|
||||
applyRosterEntry RosterMember {memberId, key = MemberKey pubKey, role} (cs, as) =
|
||||
apply `catchAllErrors` \_ -> pure (cs, as)
|
||||
where
|
||||
applied m = (cs, ((m :: GroupMember) {memberRole = role}, memberRole' m) : as)
|
||||
apply = getCreateUnknownGMByMemberId db cxt user gInfo memberId name defaultRole True >>= \case
|
||||
apply = getCreateUnknownGMByMemberId db cxt user gInfo memberId (nameFromMemberId memberId) defaultRole True >>= \case
|
||||
Nothing -> pure (cs, as)
|
||||
Just (m, _) -> case memberPubKey m of
|
||||
Just k
|
||||
@@ -3510,9 +3511,6 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
|
||||
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo''' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEMemberLeft)
|
||||
groupMsgToView cInfo ci
|
||||
toView $ CEvtLeftMember user gInfo''' m' {memberStatus = GSMemLeft} msgSigned
|
||||
-- a privileged leaver drops out of the roster
|
||||
when (useRelays' gInfo'' && memberRole' (membership gInfo'') == GROwner && isRosterRole (memberRole' m)) $
|
||||
bumpAndBroadcastRoster user gInfo'' `catchAllErrors` eToView
|
||||
pure $ memberEventDeliveryScope m
|
||||
|
||||
xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM ()
|
||||
|
||||
@@ -388,9 +388,9 @@ data InlineFileInvitation = InlineFileInvitation
|
||||
|
||||
data RosterMember = RosterMember
|
||||
{ memberId :: MemberId,
|
||||
name :: Text,
|
||||
key :: MemberKey, -- trust-on-first-use pinned per memberId
|
||||
role :: GroupMemberRole
|
||||
role :: GroupMemberRole,
|
||||
privileges :: Word16 -- reserved: serialized as 0, parsed and ignored in v1
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
@@ -398,7 +398,7 @@ data RosterMember = RosterMember
|
||||
-- The blob codec (encodeRosterBlob / rosterBlobP) is defined with maxGroupRosterSize below,
|
||||
-- so rosterBlobP and the bound it references sit in one declaration group (past the TH splices).
|
||||
instance Encoding RosterMember where
|
||||
smpEncode RosterMember {memberId, name, key, role} = smpEncode (memberId, name, key, role)
|
||||
smpEncode RosterMember {memberId, key, role, privileges} = smpEncode (memberId, key, role, privileges)
|
||||
smpP = RosterMember <$> smpP <*> smpP <*> smpP <*> smpP
|
||||
|
||||
instance Encoding FwdSender where
|
||||
@@ -930,10 +930,15 @@ maxDecompressedMsgLength :: Int
|
||||
maxDecompressedMsgLength = 65536
|
||||
|
||||
-- Defensive entry-count bound for the roster blob parser (rosterBlobP) and the
|
||||
-- promotion cap over the privileged (moderator/admin) set. The blob rides over the
|
||||
-- promotion cap over the promoted (member/moderator/admin) set. The blob rides over the
|
||||
-- inline file transfer, so it is no longer bound by maxEncodedMsgLength.
|
||||
maxGroupRosterSize :: Int
|
||||
maxGroupRosterSize = 64
|
||||
maxGroupRosterSize = 256
|
||||
|
||||
-- Receive-side byte bound: reject an owner-signed header whose claimed fileSize exceeds what
|
||||
-- maxGroupRosterSize entries can occupy (128 B/entry is a generous worst case), before a file is created.
|
||||
maxGroupRosterBytes :: Integer
|
||||
maxGroupRosterBytes = fromIntegral maxGroupRosterSize * 128
|
||||
|
||||
-- The byte sequence the owner-signed digest is computed over and verified against
|
||||
-- before parsing. Word16 count (smpEncodeList's 1-byte count is too small for the future cap).
|
||||
|
||||
@@ -68,6 +68,7 @@ module Simplex.Chat.Store.Groups
|
||||
getSupportScopeMembersByIndexes,
|
||||
getGroupModerators,
|
||||
getGroupRosterMembers,
|
||||
getGroupOnlyMembers,
|
||||
getGroupOwners,
|
||||
getGroupRelayMembers,
|
||||
getGroupMembersForExpiration,
|
||||
@@ -1224,6 +1225,14 @@ getGroupRosterMembers db cxt user@User {userId, userContactId} GroupInfo {groupI
|
||||
(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
|
||||
filter memberCurrent . map (toContactMember 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
|
||||
filter memberCurrent . map (toContactMember cxt user)
|
||||
|
||||
Reference in New Issue
Block a user