mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 19:23:36 +00:00
core: public groups - roster of privileged members (#7017)
This commit is contained in:
1 parent
5d3f016627
commit
0e09b38ea6
33 files changed
+2902
-411
No files matched your search
@@ -59,7 +59,7 @@ import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Files
|
||||
import Simplex.Chat.Markdown
|
||||
import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages, encodeBinaryBatch, encodeFwdElement)
|
||||
import Simplex.Chat.Messages.Batch (BatchMode (..), MsgBatch (..), batchMessages, encodeBatchElement, encodeBinaryBatch, encodeFwdElement)
|
||||
import Simplex.Chat.Messages.CIContent
|
||||
import Simplex.Chat.Messages.CIContent.Events
|
||||
import Simplex.Chat.Operators
|
||||
@@ -80,6 +80,7 @@ import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.Util (encryptFile, shuffle)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
|
||||
import qualified Simplex.FileTransfer.Description as FD
|
||||
import qualified Simplex.Messaging.Crypto.Lazy as LC
|
||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
||||
import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
|
||||
import Simplex.Messaging.Agent
|
||||
@@ -935,9 +936,9 @@ acceptContactRequestAsync
|
||||
liftIO $ setCommandConnId db user cmdId connId
|
||||
getContact db cxt user contactId
|
||||
|
||||
acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> CM GroupMember
|
||||
acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> Maybe GroupMember -> CM GroupMember
|
||||
acceptGroupJoinRequestAsync
|
||||
user
|
||||
user@User {userId}
|
||||
uclId
|
||||
gInfo@GroupInfo {groupProfile, membership, businessChat}
|
||||
cReqInvId
|
||||
@@ -949,12 +950,22 @@ acceptGroupJoinRequestAsync
|
||||
gAccepted
|
||||
gLinkMemRole
|
||||
incognitoProfile
|
||||
memberKey_ = do
|
||||
memberKey_
|
||||
existingMem_ = do
|
||||
gVar <- asks random
|
||||
let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted
|
||||
-- a roster-established privileged member attaches a connection to its existing record (keeping
|
||||
-- owner-authoritative role + key); everyone else is created fresh with the group-link role
|
||||
cxt <- chatStoreCxt
|
||||
(groupMemberId, memberId) <- withStore $ \db ->
|
||||
createJoiningMember db cxt gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_
|
||||
(groupMemberId, memberId) <- case existingMem_ of
|
||||
Just m -> do
|
||||
-- refresh the hash placeholder name from the authenticated join profile; role + key stay roster-authoritative
|
||||
withStore $ \db -> do
|
||||
liftIO $ updateGroupMemberStatus db userId m initialStatus
|
||||
void $ updateMemberProfile db cxt user m cReqProfile
|
||||
pure (groupMemberId' m, memberId' m)
|
||||
Nothing -> withStore $ \db ->
|
||||
createJoiningMember db cxt gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ cReqMemberId_ welcomeMsgId_ gLinkMemRole initialStatus memberKey_
|
||||
let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo
|
||||
let Profile {displayName} = userProfileInGroup user gInfo (fromIncognitoProfile <$> incognitoProfile)
|
||||
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
|
||||
@@ -1169,21 +1180,47 @@ memberIntroEvt gInfo reMember =
|
||||
mRestrictions = memberRestrictions reMember
|
||||
in XGrpMemIntro mInfo mRestrictions
|
||||
|
||||
-- Forward the saved owner-signed roster verbatim (reusing its signed shared_msg_id), then the
|
||||
-- blob chunks, so the recipient verifies the owner signature.
|
||||
serveRoster :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
serveRoster user gInfo member =
|
||||
when (member `supportsVersion` groupRosterVersion) $ do
|
||||
cxt <- chatStoreCxt
|
||||
withStore' (\db -> getGroupRoster db gInfo) >>= \case
|
||||
Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_) ->
|
||||
case J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json) of
|
||||
Left e -> logError $ "serveRoster: cannot decode saved roster message: " <> tshow e
|
||||
Right chatMsg@ChatMessage {msgId} ->
|
||||
withStore' (\db -> runExceptT $ getGroupMemberById db cxt user ownerGMId) >>= \case
|
||||
Right owner -> do
|
||||
let fwd = GrpMsgForward {fwdSender = FwdMember (memberId' owner) (memberShortenedName owner), fwdBrokerTs = brokerTs}
|
||||
sendFwdMemberMessage member fwd (VMSigned MSSVerified sm chatMsg)
|
||||
forM_ ((,) <$> msgId <*> blob_) $ \(sid, blob) ->
|
||||
sendInlineBlobChunks user gInfo [member] sid blob
|
||||
Left e -> logError $ "serveRoster: roster owner not found: " <> tshow e
|
||||
Nothing -> pure ()
|
||||
|
||||
-- Used in groups with relays to introduce moderators and above to a new member,
|
||||
-- and to announce the new member to moderators and above.
|
||||
-- This doesn't create introduction records in db, compared to above methods.
|
||||
introduceInChannel :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM ()
|
||||
introduceInChannel _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active"
|
||||
introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do
|
||||
modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo
|
||||
(owners, adminsMods) <- withStore' $ \db ->
|
||||
(,) <$> getGroupOwners db cxt user gInfo <*> getGroupAdminsMods db cxt user gInfo
|
||||
let modMs = owners <> adminsMods
|
||||
void $ sendGroupMessage' user gInfo modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing
|
||||
withStore' $ \db ->
|
||||
setMemberVectorNewRelations db subscriber [(indexInGroup m, (IDSubjectIntroduced, MRIntroduced)) | m <- modMs]
|
||||
let introEvts = map (memberIntroEvt gInfo) modMs
|
||||
forM_ (L.nonEmpty introEvts) $ \introEvts' ->
|
||||
sendGroupMemberMessages user gInfo conn introEvts'
|
||||
-- owner intros first so the joiner has the owner profile loaded before applying the saved roster (signed by the owner)
|
||||
sendIntros owners
|
||||
serveRoster user gInfo subscriber
|
||||
sendIntros adminsMods
|
||||
withStore' $ \db ->
|
||||
setMembersVectorsNewRelation db modMs subscriberIdx IDSubjectIntroduced MRIntroduced
|
||||
where
|
||||
sendIntros ms = forM_ (L.nonEmpty $ map (memberIntroEvt gInfo) ms) $ \evts ->
|
||||
sendGroupMemberMessages user gInfo conn evts
|
||||
|
||||
userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile
|
||||
userProfileInGroup user = userProfileInGroup' user . groupUserAllowSimplexLinks
|
||||
@@ -1215,6 +1252,29 @@ redactedMemberProfile allowSimplexLinks Profile {displayName, fullName, shortDes
|
||||
| hasObfuscatedSimplexLink s = Nothing
|
||||
| otherwise = maybe (Just s) (\fts -> if any ftIsSimplexLink fts then Nothing else Just s) $ parseMaybeMarkdownList s
|
||||
|
||||
-- Roles carried by the roster; owners are on the link, not the roster.
|
||||
isRosterRole :: GroupMemberRole -> Bool
|
||||
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.
|
||||
validateGroupRoster :: [RosterMember] -> [RosterMember]
|
||||
validateGroupRoster entries =
|
||||
dedup S.empty $ filter (\RosterMember {role} -> isRosterRole role) entries
|
||||
where
|
||||
dedup _ [] = []
|
||||
dedup seen (rm@RosterMember {memberId} : rms)
|
||||
| memberId `S.member` seen = dedup seen rms
|
||||
| otherwise = rm : dedup (S.insert memberId seen) rms
|
||||
|
||||
-- Privileged members without a known key are skipped (recipients can't verify them).
|
||||
buildGroupRoster :: [GroupMember] -> [RosterMember]
|
||||
buildGroupRoster mods = take maxGroupRosterSize $ mapMaybe rosterMember mods
|
||||
where
|
||||
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 ()
|
||||
sendHistory _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active"
|
||||
sendHistory user gInfo@GroupInfo {membership} m@GroupMember {activeConn = Just conn} =
|
||||
@@ -1341,7 +1401,7 @@ setGroupLinkData :: NetworkRequestMode -> User -> GroupInfo -> GroupLink -> CM G
|
||||
setGroupLinkData nm user gInfo gLink = do
|
||||
cxt <- chatStoreCxt
|
||||
(conn, groupRelays) <- withFastStore $ \db ->
|
||||
(,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo)
|
||||
(,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo)
|
||||
let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays
|
||||
linkType = if useRelays' gInfo then CCTChannel else CCTGroup
|
||||
sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData))
|
||||
@@ -1351,7 +1411,7 @@ setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM ()
|
||||
setGroupLinkDataAsync user gInfo gLink = do
|
||||
cxt <- chatStoreCxt
|
||||
(conn, groupRelays) <- withStore $ \db ->
|
||||
(,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo)
|
||||
(,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo)
|
||||
let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays
|
||||
setAgentConnShortLinkAsync user conn userLinkData (Just crClientData)
|
||||
|
||||
@@ -1628,13 +1688,16 @@ sendFileInline_ FileTransferMeta {filePath, chunkSize} sharedMsgId sendMsg =
|
||||
chSize = fromIntegral chunkSize
|
||||
|
||||
parseChatMessage :: Connection -> ByteString -> CM (ChatMessage 'Json)
|
||||
parseChatMessage conn s = do
|
||||
parseChatMessage conn s = snd <$> parseChatMessage' conn s
|
||||
{-# INLINE parseChatMessage #-}
|
||||
|
||||
parseChatMessage' :: Connection -> ByteString -> CM (Maybe SignedMsg, ChatMessage 'Json)
|
||||
parseChatMessage' conn s =
|
||||
case parseChatMessages s of
|
||||
[msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ _ m)) -> checkEncoding m) =<< msg
|
||||
[msg] -> liftEither . first (ChatError . errType) $ (\(APMsg _ (ParsedMsg _ sm m)) -> (sm,) <$> checkEncoding m) =<< msg
|
||||
_ -> throwChatError $ CEException "parseChatMessage: single message is expected"
|
||||
where
|
||||
errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s)
|
||||
{-# INLINE parseChatMessage #-}
|
||||
|
||||
getChatScopeInfo :: StoreCxt -> User -> GroupChatScope -> CM GroupChatScopeInfo
|
||||
getChatScopeInfo cxt user = \case
|
||||
@@ -1831,6 +1894,51 @@ closeFileHandle fileId files = do
|
||||
h_ <- atomically . stateTVar fs $ \m -> (M.lookup fileId m, M.delete fileId m)
|
||||
liftIO $ mapM_ hClose h_ `catchAll_` pure ()
|
||||
|
||||
-- The roster file has no chat item, so chat-item file enumeration misses it; clean it up by group.
|
||||
cleanupGroupRosterFile :: User -> GroupInfo -> CM ()
|
||||
cleanupGroupRosterFile User {userId} GroupInfo {groupId} = do
|
||||
infos <- withStore' $ \db -> getGroupRosterFileInfo db userId groupId
|
||||
forM_ infos $ \(fileId, filePath_) -> do
|
||||
lift $ closeFileHandle fileId rcvFiles
|
||||
forM_ filePath_ removeFsFile
|
||||
withStore' $ \db -> do
|
||||
deleteGroupRosterFile db userId groupId
|
||||
deleteGroupRosterTransfers db groupId
|
||||
|
||||
-- Supersede/cancel one source relay's in-flight roster transfer: remove its on-disk file + cached
|
||||
-- handle first (the cascade only does rows), then the files + transfer rows.
|
||||
cleanupRosterTransfer :: GroupInfo -> GroupMemberId -> CM ()
|
||||
cleanupRosterTransfer gInfo fromMemberId =
|
||||
withStore' (\db -> getRosterTransferId db gInfo fromMemberId) >>= mapM_ cleanupRosterTransferById
|
||||
|
||||
cleanupRosterTransferById :: Int64 -> CM ()
|
||||
cleanupRosterTransferById transferId = do
|
||||
file_ <- withStore' $ \db -> getRosterTransferFile db transferId
|
||||
forM_ file_ $ \(fileId, filePath_) -> do
|
||||
lift $ closeFileHandle fileId rcvFiles
|
||||
forM_ filePath_ removeFsFile
|
||||
withStore' $ \db -> do
|
||||
deleteRosterTransferFile db transferId
|
||||
deleteRosterTransfer db transferId
|
||||
|
||||
-- MUST evict the cached AppendMode handle before deleting chunks, else re-driven bytes append
|
||||
-- after the stale prefix and corrupt the blob.
|
||||
resetRosterPartialChunks :: RcvFileTransfer -> CM ()
|
||||
resetRosterPartialChunks ft@RcvFileTransfer {fileId, fileStatus} = do
|
||||
lift $ closeFileHandle fileId rcvFiles
|
||||
forM_ (rcvFilePath fileStatus) removeFsFile
|
||||
withStore' $ \db -> deleteRcvFileChunks db ft
|
||||
where
|
||||
rcvFilePath = \case
|
||||
RFSAccepted p -> Just p
|
||||
RFSConnected p -> Just p
|
||||
_ -> Nothing
|
||||
|
||||
removeFsFile :: FilePath -> CM ()
|
||||
removeFsFile fp = do
|
||||
p <- lift $ toFSFilePath fp
|
||||
removeFile p `catchAllErrors` \_ -> pure ()
|
||||
|
||||
deleteMembersConnections :: User -> [GroupMember] -> CM ()
|
||||
deleteMembersConnections user members = deleteMembersConnections' user members False
|
||||
|
||||
@@ -2063,6 +2171,26 @@ encodeConnInfoPQ pqSup v chatMsgEvent = do
|
||||
_ -> pure connInfo
|
||||
ECMLarge -> throwChatError $ CEException "large info"
|
||||
|
||||
-- conn-info wrapped as a signed element, so the receiver can verify the signature over the body
|
||||
encodeSignedConnInfo :: MsgEncodingI e => MsgSigning -> ChatMsgEvent e -> CM ByteString
|
||||
encodeSignedConnInfo signing chatMsgEvent = do
|
||||
vr <- chatVersionRange
|
||||
let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent}
|
||||
case encodeChatMessage maxEncodedInfoLength info of
|
||||
ECMEncoded body -> pure $ encodeBatchElement (Just $ signChatMsgBody signing body) body
|
||||
ECMLarge -> throwChatError $ CEException "large signed info"
|
||||
|
||||
-- signed XMember for a relay-group join: proves the joiner holds the member key it asserts, and carries
|
||||
-- viaRelay = the target relay's memberId inside the signed body so a sibling relay can't accept a replay
|
||||
encodeXMemberConnInfo :: GroupInfo -> MemberId -> Profile -> CM ByteString
|
||||
encodeXMemberConnInfo GroupInfo {membership = GroupMember {memberId}, groupKeys} relayMemberId profileToSend =
|
||||
case groupKeys of
|
||||
Just GroupKeys {publicGroupId, memberPrivKey} ->
|
||||
let xMemberEvt = XMember profileToSend memberId (MemberKey $ C.publicKey memberPrivKey) (Just relayMemberId)
|
||||
signing = MsgSigning CBGroup (smpEncode (publicGroupId, memberId)) KRMember memberPrivKey
|
||||
in encodeSignedConnInfo signing xMemberEvt
|
||||
Nothing -> throwChatError $ CEInternalError "no group keys for channel membership"
|
||||
|
||||
deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
|
||||
deliverMessage conn cmEventTag msgBody msgId = do
|
||||
let msgFlags = MsgFlags {notification = hasNotification cmEventTag}
|
||||
@@ -2136,6 +2264,52 @@ sendGroupMessage' user gInfo members chatMsgEvent =
|
||||
((Right msg) :| [], _) -> pure msg
|
||||
_ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message"
|
||||
|
||||
-- TODO [relays] improvement: publish roster_version in link data so the owner can recover the latest version
|
||||
-- TODO after restoring from a stale backup (relays accept only strictly-greater versions)
|
||||
-- Persist the next roster version before sending the events that carry it (so a recipient never advances
|
||||
-- past a version the owner hasn't recorded). The matching blob is broadcast separately, by broadcastRoster,
|
||||
-- after the change is applied to the owner's members - so the served roster excludes demoted/removed members.
|
||||
reserveRosterVersion :: GroupInfo -> CM VersionRoster
|
||||
reserveRosterVersion gInfo = do
|
||||
let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo)
|
||||
withStore' $ \db -> setGroupRosterVersion db gInfo rosterVer
|
||||
pure rosterVer
|
||||
|
||||
broadcastRoster :: User -> GroupInfo -> VersionRoster -> CM ()
|
||||
broadcastRoster user gInfo rosterVer = do
|
||||
cxt <- chatStoreCxt
|
||||
(relays, rosterMems) <- withStore' $ \db ->
|
||||
(,) <$> getGroupRelayMembers db cxt user gInfo <*> getGroupRosterMembers db cxt user gInfo
|
||||
forM_ (L.nonEmpty relays) $ \relays' ->
|
||||
sendRoster user gInfo (L.toList relays') rosterVer (buildGroupRoster rosterMems)
|
||||
|
||||
-- Send the current roster (no version bump) to a newly added relay so it can serve joiners.
|
||||
sendGroupRosterToRelay :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
sendGroupRosterToRelay user gInfo relayMember =
|
||||
forM_ (rosterVersion gInfo) $ \rosterVer -> do
|
||||
cxt <- chatStoreCxt
|
||||
rosterMems <- withStore' $ \db -> getGroupRosterMembers db cxt user gInfo
|
||||
sendRoster user gInfo [relayMember] rosterVer (buildGroupRoster rosterMems)
|
||||
|
||||
-- Row-less send (no files/snd_files rows, so no send-side cleanup); redelivery is the agent's.
|
||||
sendRoster :: User -> GroupInfo -> [GroupMember] -> VersionRoster -> [RosterMember] -> CM ()
|
||||
sendRoster user gInfo members rosterVer roster = do
|
||||
let blob = encodeRosterBlob roster
|
||||
fileInv = InlineFileInvitation {fileSize = fromIntegral (B.length blob), fileDigest = FD.FileDigest $ LC.sha512Hash $ LB.fromStrict blob}
|
||||
SndMessage {sharedMsgId} <- sendGroupMessage' user gInfo members (XGrpRoster GroupRoster {version = rosterVer, fileInv})
|
||||
sendInlineBlobChunks user gInfo members sharedMsgId blob
|
||||
|
||||
-- Send a binary blob as BFileChunks under a shared_msg_id to the given members (chunked by fileChunkSize).
|
||||
sendInlineBlobChunks :: User -> GroupInfo -> [GroupMember] -> SharedMsgId -> ByteString -> CM ()
|
||||
sendInlineBlobChunks user gInfo members sharedMsgId blob = do
|
||||
chSize <- fromIntegral <$> asks (fileChunkSize . config)
|
||||
go chSize 1 blob
|
||||
where
|
||||
go chSize chunkNo bytes = do
|
||||
let (chunk, rest) = B.splitAt chSize bytes
|
||||
void $ sendGroupMessage' user gInfo members (BFileChunk sharedMsgId (FileChunk chunkNo chunk))
|
||||
unless (B.null rest) $ go chSize (chunkNo + 1) rest
|
||||
|
||||
-- Relay advertises its current web preview capability to channel owners.
|
||||
-- Idempotent: sends only when the configured web domain differs from what was last sent, and only to
|
||||
-- owners whose recorded chat version supports relayWebCapVersion (older apps can't parse XGrpRelayCap).
|
||||
@@ -2372,10 +2546,14 @@ saveDirectRcvMSG conn@Connection {connId} agentMsgMeta chatMsg@ChatMessage {chat
|
||||
msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing
|
||||
pure (conn', msg)
|
||||
|
||||
saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> VerifiedMsg e -> CM (GroupMember, Connection, RcvMessage)
|
||||
saveGroupRcvMsg :: forall e. MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> VerifiedMsg e -> CM (GroupMember, Connection, RcvMessage)
|
||||
saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta verifiedMsg = do
|
||||
let ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = verifiedChatMsg verifiedMsg
|
||||
(am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- updateMemberChatVRange authorMember conn chatVRange
|
||||
-- binary messages (file chunks) carry only the initial-version sentinel, not the sender's range;
|
||||
-- applying it would downgrade the member's negotiated version and suppress version-gated delivery
|
||||
(am'@GroupMember {memberId = amMemId, groupMemberId = amGroupMemId}, conn') <- case encoding @e of
|
||||
SBinary -> pure (authorMember, conn)
|
||||
SJson -> updateMemberChatVRange authorMember conn chatVRange
|
||||
let agentMsgId = fst $ recipient agentMsgMeta
|
||||
brokerTs = metaBrokerTs agentMsgMeta
|
||||
newMsg = NewRcvMessage {chatMsgEvent, verifiedMsg, brokerTs}
|
||||
@@ -2513,11 +2691,11 @@ saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, msgSigned, forwardedByMem
|
||||
_ -> Nothing
|
||||
|
||||
-- TODO [mentions] optimize by avoiding unnecessary parsing
|
||||
mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> ChatItem c d
|
||||
mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember currentTs =
|
||||
mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> Maybe MsgSigStatus -> UTCTime -> ChatItem c d
|
||||
mkChatItem cd showGroupAsSender ciId content file quotedItem sharedMsgId itemForwarded itemTimed live userMention itemTs forwardedByMember msgSigned currentTs =
|
||||
let ts@(_, ft_) = ciContentTexts content
|
||||
hasLink_ = ciContentHasLink content ft_
|
||||
in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember Nothing currentTs
|
||||
in mkChatItem_ cd showGroupAsSender ciId content ts file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember msgSigned currentTs
|
||||
|
||||
mkChatItem_ :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ShowGroupAsSender -> ChatItemId -> CIContent d -> (Text, Maybe MarkdownList) -> Maybe (CIFile d) -> Maybe (CIQuote c) -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> Bool -> ChatItemTs -> Maybe GroupMemberId -> Maybe MsgSigStatus -> UTCTime -> ChatItem c d
|
||||
mkChatItem_ cd showGroupAsSender ciId content (itemText, formattedText) file quotedItem sharedMsgId itemForwarded itemTimed live userMention hasLink_ itemTs forwardedByMember msgSigned currentTs =
|
||||
@@ -2679,7 +2857,7 @@ createFeatureEnabledItems_ :: User -> Contact -> CM [AChatItem]
|
||||
createFeatureEnabledItems_ user ct@Contact {mergedPreferences} =
|
||||
forM allChatFeatures $ \(ACF f) -> do
|
||||
let state = featureState $ getContactUserPreference f mergedPreferences
|
||||
createChatItem user (CDDirectRcv ct) False (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing Nothing
|
||||
createChatItem user (CDDirectRcv ct) False (uncurry (CIRcvChatFeature $ chatFeature f) state) Nothing Nothing Nothing
|
||||
|
||||
createFeatureItems ::
|
||||
MsgDirectionI d =>
|
||||
@@ -2709,15 +2887,15 @@ createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do
|
||||
unless (null errs) $ toView' $ CEvtChatErrors errs
|
||||
toView' $ CEvtNewChatItems user acis
|
||||
where
|
||||
contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)])
|
||||
contactChangedFeatures :: (Contact, Contact) -> (ChatDirection 'CTDirect d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)])
|
||||
contactChangedFeatures (Contact {mergedPreferences = cups}, ct'@Contact {mergedPreferences = cups'}) = do
|
||||
let contents = mapMaybe (\(ACF f) -> featureCIContent_ f) allChatFeatures
|
||||
(chatDir ct', False, contents)
|
||||
where
|
||||
featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d, Maybe SharedMsgId)
|
||||
featureCIContent_ :: forall f. FeatureI f => SChatFeature f -> Maybe (CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)
|
||||
featureCIContent_ f
|
||||
| state /= state' = Just (fContent ciFeature state', Nothing)
|
||||
| prefState /= prefState' = Just (fContent ciOffer prefState', Nothing)
|
||||
| state /= state' = Just (fContent ciFeature state', Nothing, Nothing)
|
||||
| prefState /= prefState' = Just (fContent ciOffer prefState', Nothing, Nothing)
|
||||
| otherwise = Nothing
|
||||
where
|
||||
fContent :: FeatureContent a d -> (a, Maybe Int) -> CIContent d
|
||||
@@ -2750,16 +2928,16 @@ createGroupFeatureItems_ user cd showGroupAsSender ciContent GroupInfo {fullGrou
|
||||
forM allGroupFeatures $ \(AGF f) -> do
|
||||
let p = getGroupPreference f fullGroupPreferences
|
||||
(_, param, role) = groupFeatureState p
|
||||
createChatItem user cd showGroupAsSender (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing Nothing
|
||||
createChatItem user cd showGroupAsSender (ciContent (toGroupFeature f) (toGroupPreference p) param role) Nothing Nothing Nothing
|
||||
|
||||
createInternalChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> CIContent d -> Maybe UTCTime -> CM ()
|
||||
createInternalChatItem user cd content itemTs_ = do
|
||||
ci <- createChatItem user cd False content Nothing itemTs_
|
||||
ci <- createChatItem user cd False content Nothing Nothing itemTs_
|
||||
toView $ CEvtNewChatItems user [ci]
|
||||
|
||||
createChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Maybe UTCTime -> CM AChatItem
|
||||
createChatItem user cd showGroupAsSender content sharedMsgId itemTs_ =
|
||||
lift (createChatItems user itemTs_ [(cd, showGroupAsSender, [(content, sharedMsgId)])]) >>= \case
|
||||
createChatItem :: (ChatTypeI c, MsgDirectionI d) => User -> ChatDirection c d -> ShowGroupAsSender -> CIContent d -> Maybe SharedMsgId -> Maybe MsgSigStatus -> Maybe UTCTime -> CM AChatItem
|
||||
createChatItem user cd showGroupAsSender content sharedMsgId msgSigned itemTs_ =
|
||||
lift (createChatItems user itemTs_ [(cd, showGroupAsSender, [(content, sharedMsgId, msgSigned)])]) >>= \case
|
||||
[Right ci] -> pure ci
|
||||
[Left e] -> throwError e
|
||||
rs -> throwChatError $ CEInternalError $ "createInternalChatItem: expected 1 result, got " <> show (length rs)
|
||||
@@ -2771,7 +2949,7 @@ createChatItems ::
|
||||
(ChatTypeI c, MsgDirectionI d) =>
|
||||
User ->
|
||||
Maybe UTCTime ->
|
||||
[(ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)])] ->
|
||||
[(ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)])] ->
|
||||
CM' [Either ChatError AChatItem]
|
||||
createChatItems user itemTs_ dirsCIContents = do
|
||||
createdAt <- liftIO getCurrentTime
|
||||
@@ -2780,24 +2958,24 @@ createChatItems user itemTs_ dirsCIContents = do
|
||||
void . withStoreBatch' $ \db -> map (updateChat db cxt createdAt) dirsCIContents
|
||||
withStoreBatch' $ \db -> concatMap (createACIs db itemTs createdAt) dirsCIContents
|
||||
where
|
||||
updateChat :: DB.Connection -> StoreCxt -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> IO ()
|
||||
updateChat :: DB.Connection -> StoreCxt -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) -> IO ()
|
||||
updateChat db cxt createdAt (cd, _, contents)
|
||||
| any (ciRequiresAttention . fst) contents || contactChatDeleted cd = void $ updateChatTsStats db cxt user cd createdAt memberChatStats
|
||||
| any (\(content, _, _) -> ciRequiresAttention content) contents || contactChatDeleted cd = void $ updateChatTsStats db cxt user cd createdAt memberChatStats
|
||||
| otherwise = pure ()
|
||||
where
|
||||
memberChatStats :: Maybe (Int, MemberAttention, Int)
|
||||
memberChatStats = case cd of
|
||||
CDGroupRcv _g (Just scope) m -> do
|
||||
let unread = length $ filter (ciRequiresAttention . fst) contents
|
||||
let unread = length $ filter (\(content, _, _) -> ciRequiresAttention content) contents
|
||||
in Just (unread, memberAttentionChange unread itemTs_ (Just m) scope, 0)
|
||||
_ -> Nothing
|
||||
createACIs :: DB.Connection -> UTCTime -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> [IO AChatItem]
|
||||
createACIs :: DB.Connection -> UTCTime -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId, Maybe MsgSigStatus)]) -> [IO AChatItem]
|
||||
createACIs db itemTs createdAt (cd, showGroupAsSender, contents) = map createACI contents
|
||||
where
|
||||
createACI (content, sharedMsgId) = do
|
||||
createACI (content, sharedMsgId, msgSigned) = do
|
||||
let hasLink_ = ciContentHasLink content Nothing
|
||||
ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ itemTs createdAt
|
||||
let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing createdAt
|
||||
ciId <- createNewChatItemNoMsg db user cd showGroupAsSender content sharedMsgId hasLink_ msgSigned itemTs createdAt
|
||||
let ci = mkChatItem cd showGroupAsSender ciId content Nothing Nothing Nothing Nothing Nothing False False itemTs Nothing msgSigned createdAt
|
||||
pure $ AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci
|
||||
|
||||
-- rcvMem_ Nothing means message from channel - treated same as message from moderator,
|
||||
|
||||
Reference in new issue
Block a user