Merge branch 'master' into ep/address-dr

This commit is contained in:
Evgeny Poberezkin
2026-07-24 10:56:52 +01:00
60 changed files with 1350 additions and 260 deletions
+1 -1
View File
@@ -156,7 +156,7 @@ createActiveUser cc CoreChatOpts {chatRelay, headless} createBot_ userDisplayNam
\It is only stored on your device and you can change it later."
where
loop = do
displayName <- T.pack <$> withPrompt "display name" getLine
displayName <- T.pack <$> withPrompt "display name: " getLine
createUser loop False $ mkProfile displayName
mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
createUser onError clientService p =
+14 -13
View File
@@ -441,7 +441,6 @@ processChatCommand cxt nm = \case
where
createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO ()
createPresetContactCards db user = do
createContact db cxt user simplexStatusContactProfile
createContact db cxt user simplexTeamContactProfile
chooseServers :: Maybe User -> CM ([UpdatedUserOperatorServers], (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)))
chooseServers user_ = do
@@ -693,7 +692,8 @@ processChatCommand cxt nm = \case
(SCTGroup, SMDSnd) -> L.nonEmpty <$> withFastStore' (`getGroupSndStatuses` itemId)
_ -> pure Nothing
forwardedFromChatItem <- getForwardedFromItem user ci
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem}
fileXftpServers <- getChatItemFileServers user dir ci
pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem, fileXftpServers}
where
getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem)
getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of
@@ -2889,14 +2889,14 @@ processChatCommand cxt nm = \case
-- TODO [relays] possible optimization is to read only required members + relays
g@(Group gInfo members) <- withFastStore $ \db -> getGroup db cxt user groupId
when (selfSelected gInfo) $ throwCmdError "can't change role for self"
let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending, anyPrivilegedTarget, anyRosterChange, finalPrivilegedCount) = selectMembers members
let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending, anyPrivilegedTarget, anyRelay, anyRosterChange, finalPrivilegedCount) = selectMembers members
when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound
when (length memberIds > 1 && (anyAdmin || newRole >= GRAdmin)) $
throwCmdError "can't change role of multiple members when admins selected, or new role is admin"
when anyPending $ throwCmdError "can't change role of members pending approval"
-- TODO allow moderators (recipients already accept it; needs UI too): the observer..member limit is an
-- TODO `all` over targets - maxRole can't express it, a max hides targets below GRObserver (relay,
-- TODO unknown) that receivers reject. Fold roleRequiredToChange per target, or add allModeratable.
when (anyRelay || newRole == GRRelay) $ throwCmdError "relay role can't be changed"
-- TODO allow moderators (needs UI) - relay is rejected above (anyRelay), so drop the GRAdmin floor:
-- TODO assertUserGroupRole gInfo (roleRequiredToChange maxRole newRole)
assertUserGroupRole gInfo $ maximum ([GRAdmin, maxRole, newRole] :: [GroupMemberRole])
-- in relay groups the roster has a single signer, so only the owner may change member/moderator/admin roles
when (useRelays' gInfo && (isRosterRole newRole || anyPrivilegedTarget) && memberRole' (membership gInfo) /= GROwner) $
@@ -2917,22 +2917,23 @@ processChatCommand cxt nm = \case
-- anyPrivilegedTarget: a target currently member/moderator/admin (gates the owner-only check); anyRosterChange:
-- a current member's role change that alters the roster blob - the only case that bumps the version, since a
-- bump with no delta reads as a gap to subscribers; finalPrivilegedCount: moderators + admins after the change.
selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool, Bool, Int)
selectMembers = foldr' addMember ([], [], [], GRObserver, False, False, False, False, 0)
selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool, Bool, Bool, Bool, Bool, Int)
selectMembers = foldr' addMember ([], [], [], GRObserver, False, False, False, False, False, 0)
where
addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRosterChange, privCount)
addMember m@GroupMember {groupMemberId, memberStatus, memberRole} (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRelay, anyRosterChange, privCount)
| groupMemberId `elem` memberIds =
let maxRole' = max maxRole memberRole
anyAdmin' = anyAdmin || memberRole >= GRAdmin
anyPending' = anyPending || memberPending m
anyPrivTarget' = anyPrivTarget || isRosterRole memberRole
anyRelay' = anyRelay || memberRole == GRRelay
privCount' = if isRosterRole newRole then privCount + 1 else privCount
in if
| memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRosterChange, privCount')
| memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRosterChange, privCount')
| memberRole == newRole -> (invited, current, m : unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRelay', anyRosterChange, privCount')
| memberStatus == GSMemInvited -> (m : invited, current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRelay', anyRosterChange, privCount')
-- a current member's role actually changes here; it alters the roster iff the old or new role is on it
| otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRosterChange || isRosterRole newRole || isRosterRole memberRole, privCount')
| otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRosterChange, if isRosterRole memberRole then privCount + 1 else privCount)
| otherwise -> (invited, m : current, unchanged, maxRole', anyAdmin', anyPending', anyPrivTarget', anyRelay', anyRosterChange || isRosterRole newRole || isRosterRole memberRole, privCount')
| otherwise = (invited, current, unchanged, maxRole, anyAdmin, anyPending, anyPrivTarget, anyRelay, anyRosterChange, if isRosterRole memberRole then privCount + 1 else privCount)
changeRoleInvitedMems :: User -> GroupInfo -> [GroupMember] -> CM ([ChatError], [GroupMember])
changeRoleInvitedMems user gInfo memsToChange = do
-- not batched, as we need to send different invitations to different connections anyway
File diff suppressed because one or more lines are too long
+12
View File
@@ -3337,6 +3337,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
xGrpMemRole :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpMemRole gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId memRole memberKey_ rosterVer_ msg@RcvMessage {msgSigned} brokerTs
| memRole == GRRelay = messageError "x.grp.mem.role: relay role can't be assigned" $> Nothing
| membershipMemId == memId =
applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $
let gInfo' = gInfo {membership = membership {memberRole = memRole}}
@@ -3363,6 +3364,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
-- applyMember writes the change (role, or role + pinned key for a freshly TOFU-created member);
-- the delivery scope (relay forwarding) is computed on the pre-change role
changeMemberRole gInfo' member@GroupMember {memberRole = fromRole} created applyMember gEvent createItem
| fromRole == GRRelay =
messageError "x.grp.mem.role: relay role can't be changed" $> Nothing
| senderRole < roleRequiredToChange fromRole memRole =
messageError "x.grp.mem.role with insufficient member permissions" $> Nothing
| useRelays' gInfo && (isRosterRole memRole || isRosterRole fromRole) && senderRole /= GROwner =
@@ -3873,6 +3876,8 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
Just (author, unknown)
| memberRemoved author ->
logInfo $ "x.grp.msg.forward: ignoring content from removed member, group " <> tshow (groupId' gInfo) <> ", member " <> safeDecodeUtf8 (strEncode memberId) <> ", event " <> tshow (toCMEventTag chatMsgEvent)
| not (useRelays' gInfo) && not (expectedForwarder author) ->
logInfo $ "x.grp.msg.forward: ignoring content from unexpected forwarder, group " <> tshow (groupId' gInfo) <> ", forwarder " <> tshow (groupMemberId' m) <> ", member " <> safeDecodeUtf8 (strEncode memberId) <> ", event " <> tshow (toCMEventTag chatMsgEvent)
| otherwise -> do
when unknown $ toView $ CEvtUnknownMemberCreated user gInfo m author
void $ withVerifiedMsg gInfo scopeInfo author parsedMsg msgTs $
@@ -3880,6 +3885,13 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
Nothing -> pure ()
FwdChannel -> processForwardedMsg (VMUnsigned chatMsg) Nothing
where
-- Forwards are only expected from the member that introduced us to the author: our host, or
-- the author's inviter. Unknown members have no such record, so any admin may forward theirs.
expectedForwarder :: GroupMember -> Bool
expectedForwarder author =
memberCategory m == GCHostMember
|| invitedByGroupMemberId author == Just (groupMemberId' m)
|| memberStatus author == GSMemUnknown
-- ! see isForwardedGroupMsg: forwarded group events should include msgId to be deduplicated
processForwardedMsg :: VerifiedMsg 'Json -> Maybe GroupMember -> CM ()
processForwardedMsg verifiedMsg author_ = do
+3 -2
View File
@@ -55,7 +55,7 @@ import Simplex.Messaging.Crypto.File (CryptoFile (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, sumTypeJSON)
import Simplex.Messaging.Protocol (BlockingInfo, MsgBody)
import Simplex.Messaging.Protocol (BlockingInfo, MsgBody, XFTPServer)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>))
data ChatType = CTDirect | CTGroup | CTLocal | CTContactRequest | CTContactConnection
@@ -1345,7 +1345,8 @@ instance TextEncoding CIForwardedFromTag where
data ChatItemInfo = ChatItemInfo
{ itemVersions :: [ChatItemVersion],
memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus),
forwardedFromChatItem :: Maybe AChatItem
forwardedFromChatItem :: Maybe AChatItem,
fileXftpServers :: [XFTPServer]
}
deriving (Show)
+7
View File
@@ -815,6 +815,13 @@ isVoice = \case
MCVoice {} -> True
_ -> False
isMedia :: MsgContent -> Bool
isMedia = \case
MCImage {} -> True
MCVideo {} -> True
MCFile {} -> True
_ -> False
isReport :: MsgContent -> Bool
isReport = \case
MCReport {} -> True
+10
View File
@@ -20,6 +20,7 @@ module Simplex.Chat.Store.Files
createSndFileTransferXFTP,
createSndFTDescrXFTP,
setSndFTPrivateSndDescr,
getSndFTPrivateSndDescr,
updateSndFTDescrXFTP,
createExtraSndFTDescrs,
updateSndFTDeliveryXFTP,
@@ -210,6 +211,15 @@ setSndFTPrivateSndDescr db User {userId} fileId sfdText = do
"UPDATE files SET private_snd_file_descr = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
(sfdText, currentTs, userId, fileId)
getSndFTPrivateSndDescr :: DB.Connection -> User -> FileTransferId -> IO (Maybe Text)
getSndFTPrivateSndDescr db User {userId} fileId =
fmap (maybe Nothing fromOnly) $
maybeFirstRow id $
DB.query
db
"SELECT private_snd_file_descr FROM files WHERE user_id = ? AND file_id = ?"
(userId, fileId)
updateSndFTDescrXFTP :: DB.Connection -> User -> SndFileTransfer -> Text -> IO ()
updateSndFTDescrXFTP db user@User {userId} sft@SndFileTransfer {fileId, fileDescrId} rfdText = do
currentTs <- getCurrentTime
+2
View File
@@ -2093,6 +2093,7 @@ updateGroupChatItemsRead db User {userId} GroupInfo {groupId} = do
[sql|
UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ?
WHERE user_id = ? AND group_id = ?
AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL
AND item_status = ?
|]
(CISRcvRead, currentTs, userId, groupId, CISRcvNew)
@@ -2149,6 +2150,7 @@ getGroupUnreadTimedItems db User {userId} groupId scope =
SELECT chat_item_id, timed_ttl
FROM chat_items
WHERE user_id = ? AND group_id = ?
AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL
AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL
|]
(userId, groupId, CISRcvNew)
@@ -1586,10 +1586,11 @@ Query:
SELECT chat_item_id, timed_ttl
FROM chat_items
WHERE user_id = ? AND group_id = ?
AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL
AND item_status = ? AND timed_ttl IS NOT NULL AND timed_delete_at IS NULL
Plan:
SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?)
SEARCH chat_items USING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?)
Query:
SELECT chat_item_moderation_id, moderator_member_id, created_by_msg_id, moderated_at
@@ -5042,10 +5043,11 @@ SEARCH chat_items USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ?
WHERE user_id = ? AND group_id = ?
AND group_scope_tag IS NULL AND group_scope_group_member_id IS NULL
AND item_status = ?
Plan:
SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=? AND group_id=? AND item_status=?)
SEARCH chat_items USING INDEX idx_chat_items_group_scope_stats_all (user_id=? AND group_id=? AND group_scope_tag=? AND group_scope_group_member_id=? AND item_status=?)
Query:
UPDATE chat_items SET item_status = ?, item_viewed = 1, updated_at = ?