core: refactor groups (#7503)

* core: refactor groups

* refactor

* refactor

* refactor

* refactor

* refactor

* rename

* refactor

* remove

* rename

* diff

* simplify

* relay requests

* bot types

* check useRelays

* refactor

* query plans

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
Evgeny
2026-09-18 10:18:10 +01:00
committed by GitHub
co-authored by Evgeny @ SimpleX Chat
parent c7b3e362ec
commit c8466b2010
23 changed files with 916 additions and 640 deletions
+1 -6
View File
@@ -4,8 +4,7 @@
{-# LANGUAGE ScopedTypeVariables #-}
module Simplex.Chat.Bot.Store
( storeCxt,
withDB,
( withDB,
withDB',
)
where
@@ -20,10 +19,6 @@ import Simplex.Messaging.Agent.Store.Common (withTransaction)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Util (catchAll)
storeCxt :: ChatController -> StoreCxt
storeCxt ChatController {config} = mkStoreCxt config
{-# INLINE storeCxt #-}
withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Either String a)
withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a
+3 -3
View File
@@ -228,9 +228,9 @@ newWebPreviewState = do
-- | Builds the read-only context threaded through store functions from chat config.
-- The single construction point, so new store-wide config (e.g. server keys) is added in one place.
mkStoreCxt :: ChatConfig -> StoreCxt
mkStoreCxt ChatConfig {chatVRange, badgePublicKeys} = StoreCxt chatVRange badgePublicKeys
{-# INLINE mkStoreCxt #-}
storeCxt :: ChatController -> StoreCxt
storeCxt ChatController {config = ChatConfig {chatVRange, badgePublicKeys}, random} = StoreCxt chatVRange badgePublicKeys random
{-# INLINE storeCxt #-}
data RandomAgentServers = RandomAgentServers
{ smpServers :: NonEmpty (ServerCfg 'PSMP),
+132 -137
View File
@@ -753,8 +753,8 @@ processChatCommand cxt nm = \case
Nothing -> pure ()
withGroupLock "sendMessage" chatId $ do
(gInfo, cmrs) <- withFastStore $ \db -> do
g <- getGroupInfo db cxt user chatId
(g,) <$> mapM (composedMessageReqMentions db user g) cms
gik@(GIK g _) <- getGroupInfoKeys db cxt user chatId
(gik,) <$> mapM (composedMessageReqMentions db user g) cms
sendGroupContentMessages user gInfo gsScope asGroup live itemTTL sign cmrs
APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do
_ <- createChatTag db user emoji text
@@ -783,7 +783,7 @@ processChatCommand cxt nm = \case
createNoteFolderContentItems user folderId (L.map composedMessageReq cms)
APIReportMessage gId reportedItemId reportReason reportText -> withUser $ \user ->
withGroupLock "reportMessage" gId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId
gInfo <- withFastStore $ \db -> getGroupInfoKeys db cxt user gId
let mc = MCReport reportText reportReason
cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty}
sendGroupContentMessages user gInfo (Just $ GCSMemberSupport Nothing) False False Nothing False [composedMessageReq cm]
@@ -818,7 +818,7 @@ processChatCommand cxt nm = \case
_ -> throwChatError CEInvalidChatItemUpdate
CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate
CTGroup -> withGroupLock "updateChatItem" chatId $ do
gInfo@GroupInfo {groupId, membership} <- withFastStore $ \db -> getGroupInfo db cxt user chatId
g@(GIK gInfo@GroupInfo {groupId, membership} _) <- withFastStore $ \db -> getGroupInfoKeys db cxt user chatId
when (isNothing scope) $ assertUserGroupRole gInfo GRAuthor
let (_, ft_) = msgContentTexts mc
if prohibitedSimplexLinks gInfo membership mc ft_
@@ -840,7 +840,7 @@ processChatCommand cxt nm = \case
mentions' = M.map (\CIMention {memberId} -> MsgMention {memberId}) ciMentions
event = XMsgUpdate itemSharedMId mc mentions' (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive) msgScope (Just showGroupAsSender)
reuseSign = case msgVerified of Just (MVSigned _) -> True; _ -> False
SndMessage {msgId} <- sendGroupMessage user gInfo scope recipients reuseSign event
SndMessage {msgId} <- sendGroupMessage user g scope recipients reuseSign event
ci' <- withFastStore' $ \db -> do
currentTs <- liftIO getCurrentTime
when changed $
@@ -886,7 +886,7 @@ processChatCommand cxt nm = \case
else markDirectCIsDeleted user ct items =<< liftIO getCurrentTime
pure $ CRChatItemsDeleted user deletions True False
CTGroup -> withGroupLock "deleteChatItem" chatId $ do
(gInfo, items) <- getCommandGroupChatItems user chatId itemIds
(g@(GIK gInfo _), items) <- getCommandGroupChatItems user chatId itemIds
-- TODO [knocking] check scope for all items?
chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope
deletions <- case mode of
@@ -899,14 +899,14 @@ processChatCommand cxt nm = \case
recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion
assertDeletable items
assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier
let signedEvents = L.nonEmpty $ mapMaybe (delEventSigned gInfo chatScopeInfo False) items
mapM_ (sendGroupSignedMessages user gInfo Nothing False recipients) signedEvents
let signedEvents = L.nonEmpty $ mapMaybe (delEventSigned g chatScopeInfo False) items
mapM_ (sendGroupSignedMessages user g Nothing False recipients) signedEvents
delGroupChatItems user gInfo chatScopeInfo items False
CIDMHistory -> do
unless (publicGroupEditor gInfo (membership gInfo)) $ throwChatError CEInvalidChatItemDelete
recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion
let signedEvents = L.nonEmpty $ mapMaybe (delEventSigned gInfo chatScopeInfo True) items
mapM_ (sendGroupSignedMessages user gInfo Nothing False recipients) signedEvents
let signedEvents = L.nonEmpty $ mapMaybe (delEventSigned g chatScopeInfo True) items
mapM_ (sendGroupSignedMessages user g Nothing False recipients) signedEvents
delGroupChatItems user gInfo chatScopeInfo items False
pure $ CRChatItemsDeleted user deletions True False
CTLocal -> do
@@ -929,20 +929,20 @@ processChatCommand cxt nm = \case
itemsMsgIds :: [CChatItem c] -> [SharedMsgId]
itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId)
-- history delete always signs (attributable owner action); self-delete signs iff the target was held signed (deniability)
delEventSigned :: GroupInfo -> Maybe GroupChatScopeInfo -> Bool -> CChatItem 'CTGroup -> Maybe (Maybe MsgSigning, ChatMsgEvent 'Json)
delEventSigned gInfo chatScopeInfo onlyHistory (CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId, msgVerified}}) =
delEventSigned :: GroupInfoKeys -> Maybe GroupChatScopeInfo -> Bool -> CChatItem 'CTGroup -> Maybe (Maybe MsgSigning, ChatMsgEvent 'Json)
delEventSigned g@(GIK gInfo _) chatScopeInfo onlyHistory (CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId, msgVerified}}) =
delEvent <$> itemSharedMsgId
where
delEvent msgId =
let evt = XMsgDel msgId Nothing (toMsgScope gInfo <$> chatScopeInfo) onlyHistory
in (groupMsgSigning (onlyHistory || itemSigned) gInfo evt, evt)
in (groupMsgSigning (onlyHistory || itemSigned) g evt, evt)
itemSigned = case msgVerified of Just (MVSigned _) -> True; _ -> False
APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do
(gInfo, items) <- getCommandGroupChatItems user gId itemIds
(g@(GIK gInfo _), items) <- getCommandGroupChatItems user gId itemIds
-- TODO [knocking] check scope is Nothing for all items? (prohibit moderation in support chats?)
ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo
let recipients = filter memberCurrent ms
deletions <- delGroupChatItemsForMembers user gInfo Nothing recipients items
deletions <- delGroupChatItemsForMembers user g Nothing recipients items
pure $ CRChatItemsDeleted user deletions True False
APIArchiveReceivedReports gId -> withUser $ \user -> withFastStore $ \db -> do
g <- getGroupInfo db cxt user gId
@@ -950,7 +950,7 @@ processChatCommand cxt nm = \case
ciIds <- liftIO $ markReceivedGroupReportsDeleted db user g deleteTs
pure $ CRGroupChatItemsDeleted user g ciIds True (Just $ membership g)
APIDeleteReceivedReports gId itemIds mode -> withUser $ \user -> withGroupLock "deleteReports" gId $ do
(gInfo, items) <- getCommandGroupChatItems user gId itemIds
(g@(GIK gInfo _), items) <- getCommandGroupChatItems user gId itemIds
unless (all isRcvReport items) $ throwCmdError "some items are not received reports"
-- TODO [knocking] scope can be different for each item if reports are from different members
-- TODO (currently we pass Nothing as scope which is wrong)
@@ -961,7 +961,7 @@ processChatCommand cxt nm = \case
CIDMBroadcast -> do
ms <- withFastStore' $ \db -> getGroupModerators db cxt user gInfo
let recipients = filter memberCurrent ms
delGroupChatItemsForMembers user gInfo Nothing recipients items
delGroupChatItemsForMembers user g Nothing recipients items
pure $ CRChatItemsDeleted user deletions True False
where
isRcvReport = \case
@@ -990,9 +990,9 @@ processChatCommand cxt nm = \case
CTGroup ->
withGroupLock "chatItemReaction" chatId $ do
-- TODO [knocking] check chat item scope?
(g@GroupInfo {membership}, CChatItem md ci) <- withFastStore $ \db -> do
g <- getGroupInfo db cxt user chatId
(g,) <$> getGroupCIWithReactions db user g itemId
(gik@(GIK g@GroupInfo {membership} _), CChatItem md ci) <- withFastStore $ \db -> do
gik@(GIK g _) <- getGroupInfoKeys db cxt user chatId
(gik,) <$> getGroupCIWithReactions db user g itemId
chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope
recipients <- getGroupRecipients cxt user g chatScopeInfo groupKnockingVersion
case ci of
@@ -1004,7 +1004,7 @@ processChatCommand cxt nm = \case
let itemMemberId = memberId' <$> chatItemMember g ci
rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True
checkReactionAllowed rs
SndMessage {msgId} <- sendGroupMessage user g scope recipients False (XMsgReact itemSharedMId itemMemberId (toMsgScope g <$> chatScopeInfo) reaction add)
SndMessage {msgId} <- sendGroupMessage user gik scope recipients False (XMsgReact itemSharedMId itemMemberId (toMsgScope g <$> chatScopeInfo) reaction add)
createdAt <- liftIO getCurrentTime
reactions <- withFastStore' $ \db -> do
setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt
@@ -1089,7 +1089,7 @@ processChatCommand cxt nm = \case
case L.nonEmpty cmrs of
Just cmrs' ->
withGroupLock "forwardChatItem, to group" toChatId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db cxt user toChatId
gInfo <- withFastStore $ \db -> getGroupInfoKeys db cxt user toChatId
sendGroupContentMessages user gInfo toScope sendAsGroup False itemTTL False cmrs'
Nothing -> pure $ CRNewChatItems user []
CTLocal -> do
@@ -1119,7 +1119,7 @@ processChatCommand cxt nm = \case
| otherwise = displayName
-- TODO [knocking] from scope?
CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do
(gInfo, items) <- getCommandGroupChatItems user fromChatId itemIds
(GIK gInfo _, items) <- getCommandGroupChatItems user fromChatId itemIds
catMaybes <$> mapM (\ci -> ciComposeMsgReq gInfo ci <$$> prepareMsgReq ci) items
where
ciComposeMsgReq :: GroupInfo -> CChatItem 'CTGroup -> (MsgContent, Maybe CryptoFile) -> ComposedMessageReq
@@ -1226,16 +1226,16 @@ processChatCommand cxt nm = \case
let ext = takeExtension fileName
pure $ prefix <> formattedDate <> ext
APIShareChatMsgContent (ChatRef CTGroup groupId _) toSendRef -> withUser $ \user -> do
GroupInfo {groupProfile = gp@GroupProfile {publicGroup}, membership = GroupMember {memberId, memberRole}, groupKeys} <-
withFastStore $ \db -> getGroupInfo db cxt user groupId
GIK GroupInfo {groupProfile = gp@GroupProfile {publicGroup}, membership = GroupMember {memberId, memberRole}} gks <-
withFastStore $ \db -> getGroupInfoKeys db cxt user groupId
case publicGroup of
Nothing -> throwCmdError "not a public group"
Just PublicGroupProfile {groupLink} -> do
let signingKeys = case (memberRole, groupKeys) of
(GROwner, Just gk@GroupKeys {publicGroupKeys = Just PublicGroupKeys {groupRootKey = GRKPrivate _}}) -> Just gk
let signingKeys = case (memberRole, gks) of
(GROwner, GKPublicGroup {groupRootKey = GRKPrivate _, memberPrivKey}) -> Just memberPrivKey
_ -> Nothing
ownerSig <-
pure signingKeys $>>= \GroupKeys {memberPrivKey} ->
pure signingKeys $>>= \memberPrivKey ->
mkLinkOwnerSig memberPrivKey groupLink (Just memberId) <$$> shareChatBinding user toSendRef
let text = safeDecodeUtf8 $ strEncode groupLink
pure $ CRChatMsgContent user MCChat {text, chatLink = MCLGroup groupLink gp, ownerSig}
@@ -1378,7 +1378,7 @@ processChatCommand cxt nm = \case
withFastStore' $ \db -> deletePendingContactConnection db userId chatId
pure $ CRContactConnectionDeleted user conn
CTGroup | isNothing scope -> do
gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db cxt user chatId
g@(GIK gInfo@GroupInfo {membership} _) <- withFastStore $ \db -> getGroupInfoKeys db cxt user chatId
let isOwner = memberRole' membership == GROwner
canDelete = isOwner || not (memberCurrent membership)
unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner
@@ -1391,7 +1391,7 @@ processChatCommand cxt nm = \case
let doSendDel = memberActive membership && isOwner
msgSigned <-
if doSendDel
then (\SndMessage {signedMsg_} -> isJust signedMsg_) <$> sendGroupMessage' user gInfo recipients XGrpDel
then (\SndMessage {signedMsg_} -> isJust signedMsg_) <$> sendGroupMessage' user g recipients XGrpDel
else pure False
deleteGroupLinkIfExists user gInfo
deleteMembersConnections' user members doSendDel
@@ -2293,7 +2293,7 @@ processChatCommand cxt nm = \case
pure $ CRStartedConnectionToContact user ct' customUserProfile
CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct'
APIConnectPreparedGroup {groupId, incognito, ownerContact, msgContent_} -> withUser $ \user -> do
gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId
g@(GIK gInfo _) <- withFastStore $ \db -> getGroupInfoKeys db cxt user groupId
case gInfo of
GroupInfo {preparedGroup = Nothing} -> throwCmdError "group doesn't have link to connect"
GroupInfo {useRelays = BoolDef True, preparedGroup = Just PreparedGroup {connLinkToConnect}} -> do
@@ -2313,9 +2313,8 @@ processChatCommand cxt nm = \case
-- set group link info and incognito profile, generate and store membership keys
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
let cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} e2e
(_, memberPrivKey) <- atomically . C.generateKeyPair =<< asks random
gInfo' <- withFastStore $ \db -> do
gInfo' <- updatePreparedRelayedGroup db cxt user gInfo mainCReq cReqHash incognitoProfile rootKey memberPrivKey publicMemberCount_
g'@(GIK gInfo' _) <- withFastStore $ \db -> do
g'@(GIK gInfo' _) <- updatePreparedRelayedGroup db cxt user gInfo mainCReq cReqHash incognitoProfile rootKey publicMemberCount_
-- Pre-emptively create owner members with trusted keys from link data
forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> do
let ctId_ = case ownerContact of
@@ -2323,9 +2322,9 @@ processChatCommand cxt nm = \case
| memberId == MemberId ownerId -> Just contactId
_ -> Nothing
void $ createLinkOwnerMember db cxt user gInfo' ctId_ (MemberId ownerId) ownerKey
pure gInfo'
pure g'
rs <- withGroupLock "connectPreparedGroup" groupId $
mapConcurrently (connectToRelay user gInfo') relays
mapConcurrently (connectToRelay user g') relays
let relayFailed = \case (_, _, Left _) -> True; _ -> False
(failed, succeeded) = partition relayFailed rs
if null succeeded
@@ -2367,7 +2366,7 @@ processChatCommand cxt nm = \case
smId <- getSharedMsgId
withFastStore' $ \db -> setRequestSharedMsgIdForGroup db groupId smId
pure (smId, mc)
r <- connectViaContact user (Just $ PCEGroup gInfo hostMember) incognito connLinkToConnect welcomeSharedMsgId msg_ `catchAllErrors` \e -> do
r <- connectViaContact user (Just $ PCEGroup g hostMember) incognito connLinkToConnect welcomeSharedMsgId msg_ `catchAllErrors` \e -> do
-- get updated group info, in case connection was started (connLinkPreparedConnection) - in UI it would lock ability to change
-- user or incognito profile for group or business chat, in case server received request while client got network error
gInfo' <- withFastStore $ \db -> getGroupInfo db cxt user groupId
@@ -2699,14 +2698,14 @@ processChatCommand cxt nm = \case
g <- asks random
memberId <- liftIO $ MemberId <$> encodedRandomBytes g 12
(_, memberPrivKey) <- atomically $ C.generateKeyPair g
gInfo <- newGroup user incognito gProfile False memberId (Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey}) Nothing
gInfo <- newGroup user incognito gProfile memberId GKGroup {memberPrivKey} Nothing
createNewGroupItems user gInfo
pure $ CRGroupCreated user gInfo
NewGroup incognito gProfile -> withUser $ \User {userId} ->
processChatCommand cxt nm $ APINewGroup userId incognito gProfile
APINewPublicGroup userId incognito relayIds groupProfile -> withUserId userId $ \user -> do
(gProfile', memberId, groupKeys, setupLink) <- prepareGroupLink user
gInfo <- newGroup user incognito gProfile' True memberId (Just groupKeys) (Just 1)
gInfo <- newGroup user incognito gProfile' memberId groupKeys (Just 1)
(gLink, results) <- setupLink gInfo `catchAllErrors` \e -> do
deleteInProgressGroup user gInfo
throwError e
@@ -2753,8 +2752,7 @@ processChatCommand cxt nm = \case
userLinkData = UserContactLinkData UserContactData {direct = False, owners = [ownerAuth], relays = [], userData, ratchetKeys = Nothing}
-- create connection with prepared link (single network call)
connId <- withAgent $ \a -> createConnectionForLink a nm (aUserId user) True ccLink preparedParams userLinkData subMode
let groupKeys = GroupKeys {publicGroupKeys, memberPrivKey}
publicGroupKeys = Just PublicGroupKeys {publicGroupId = B64UrlByteString entityId, groupRootKey = GRKPrivate rootPrivKey}
let groupKeys = GKPublicGroup {groupRootKey = GRKPrivate rootPrivKey, memberPrivKey}
setupLink gInfo = do
-- TODO [relays] starting role should be communicated in protocol from owner to relays
subRole <- asks $ channelSubscriberRole . config
@@ -2804,7 +2802,7 @@ processChatCommand cxt nm = \case
_ -> False
APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do
-- TODO for large groups: no need to load all members to determine if contact is a member
(group, contact) <- withFastStore $ \db -> (,) <$> getGroup db cxt user groupId <*> getContact db cxt user contactId
((group, gks), contact) <- withFastStore $ \db -> (,) <$> getGroupKeys_ db cxt user groupId <*> getContact db cxt user contactId
let Group gInfo members = group
Contact {localDisplayName = cName} = contact
when (useRelays' gInfo) $ throwCmdError "can't invite contact to channel"
@@ -2814,7 +2812,7 @@ processChatCommand cxt nm = \case
when (contactConnIncognito contact) $ throwChatError CEContactIncognitoCantInvite
-- [incognito] forbid to invite contacts if user joined the group using an incognito profile
when (incognitoMembership gInfo) $ throwChatError CEGroupIncognitoCantInvite
let sendInvitation = sendGrpInvitation user contact gInfo
let sendInvitation = sendGrpInvitation user contact (GIK gInfo gks)
case contactMember contact members of
Nothing -> do
gVar <- asks random
@@ -2837,13 +2835,13 @@ processChatCommand cxt nm = \case
(invitation, ct) <- withFastStore $ \db -> do
inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db cxt user groupId
(inv,) <$> getContactViaMember db cxt user fromMember
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}} = invitation
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}, groupKeys = gks} = invitation
GroupMember {memberId = membershipMemId} = membership
Contact {activeConn} = ct
case activeConn of
Just Connection {peerChatVRange} -> do
subMode <- chatReadVar subscriptionMode
dm <- encodeConnInfo $ XGrpAcpt membershipMemId (groupMemberKey g)
dm <- encodeConnInfo $ XGrpAcpt membershipMemId (Just $ groupMemberKey gks)
agentConnId <- case memberConn fromMember of
Nothing -> do
agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff
@@ -2866,7 +2864,7 @@ processChatCommand cxt nm = \case
pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing
Nothing -> throwChatError $ CEContactNotActive ct
APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do
(gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user groupId <*> getGroupMemberById db cxt user gmId
(g@(GIK gInfo _), m) <- withFastStore $ \db -> (,) <$> getGroupInfoKeys db cxt user groupId <*> getGroupMemberById db cxt user gmId
assertUserGroupRole gInfo $ max GRModerator role
case memberStatus m of
GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve
@@ -2875,14 +2873,14 @@ processChatCommand cxt nm = \case
Just mConn ->
case memberAdmission >>= review of
Just MCAll -> do
introduceToModerators cxt user gInfo m
introduceToModerators cxt user g m
withFastStore' $ \db -> updateGroupMemberStatus db userId m GSMemPendingReview
let m' = m {memberStatus = GSMemPendingReview}
pure $ CRMemberAccepted user gInfo m'
Nothing -> do
let msg = XGrpLinkAcpt GAAccepted role (memberId' m)
void $ sendDirectMemberMessage mConn msg groupId
introduceToRemaining cxt user gInfo m {memberRole = role}
introduceToRemaining cxt user g m {memberRole = role}
when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m
(m', gInfo') <- withFastStore' $ \db -> do
m' <- updateGroupMemberAccepted db user m GSMemConnected role
@@ -2900,13 +2898,13 @@ processChatCommand cxt nm = \case
modMs <- withFastStore' $ \db -> getGroupModerators db cxt user gInfo
let rcpModMs' = filter memberCurrent modMs
msg = XGrpLinkAcpt GAAccepted role (memberId' m)
void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') False msg
void $ sendGroupMessage user g scope ([m] <> rcpModMs') False msg
when (maxVersion (memberChatVRange m) < groupKnockingVersion) $
forM_ (memberConn m) $ \mConn -> do
let msg2 = XMsgNew $ mcSimple (MCText acceptedToGroupMessage)
void $ sendDirectMemberMessage mConn msg2 groupId
when (memberCategory m == GCInviteeMember) $ do
introduceToRemaining cxt user gInfo m {memberRole = role}
introduceToRemaining cxt user g m {memberRole = role}
when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m
(m', gInfo') <- withFastStore' $ \db -> do
m' <- updateGroupMemberAccepted db user m newMemberStatus role
@@ -2938,7 +2936,7 @@ processChatCommand cxt nm = \case
APIMembersRole groupId memberIds newRole -> withUser $ \user ->
withGroupLock "memberRole" groupId $ do
-- TODO [relays] possible optimization is to read only required members + relays
g@(Group gInfo members) <- withFastStore $ \db -> getGroup db cxt user groupId
(g@(Group gInfo members), gks) <- withFastStore $ \db -> getGroupKeys_ db cxt user groupId
when (selfSelected gInfo) $ throwCmdError "can't change role for self"
let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending, anyPrivilegedTarget, anyRelay, anyRosterChange, finalPrivilegedCount) = selectMembers members
when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound
@@ -2954,11 +2952,11 @@ processChatCommand cxt nm = \case
throwCmdError "only the group owner can change moderator and admin roles"
when (useRelays' gInfo && isRosterRole newRole && finalPrivilegedCount > maxGroupRosterSize) $
throwCmdError $ "the number of members, moderators and admins would exceed the limit of " <> show maxGroupRosterSize
(errs1, changed1) <- changeRoleInvitedMems user gInfo invitedMems
(errs1, changed1) <- changeRoleInvitedMems user (GIK gInfo gks) invitedMems
let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyRosterChange
-- roster (with the change projected in) before the delta, so a relay stores the blob at this version before forwarding the delta
rosterVer <- if doBumpRoster then Just <$> broadcastRoster user gInfo (RDRoleChanged newRole currentMems) else pure Nothing
(errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g rosterVer currentMems
rosterVer <- if doBumpRoster then Just <$> broadcastRoster user (GIK gInfo gks) (RDRoleChanged newRole currentMems) else pure Nothing
(errs2, changed2, acis, msgSigned) <- changeRoleCurrentMems user g gks rosterVer currentMems
unless (null acis) $ toView $ CEvtNewChatItems user acis
let errs = errs1 <> errs2
unless (null errs) $ toView $ CEvtChatErrors errs
@@ -2985,8 +2983,8 @@ processChatCommand cxt nm = \case
-- 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', 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
changeRoleInvitedMems :: User -> GroupInfoKeys -> [GroupMember] -> CM ([ChatError], [GroupMember])
changeRoleInvitedMems user gInfo@(GIK g _) memsToChange = do
-- not batched, as we need to send different invitations to different connections anyway
mems_ <- forM memsToChange $ \m -> (Right <$> changeRole m) `catchAllErrors` (pure . Left)
pure $ partitionEithers mems_
@@ -2998,15 +2996,15 @@ processChatCommand cxt nm = \case
sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = newRole} cReq
withFastStore' $ \db -> updateGroupMemberRole db user m newRole
pure (m :: GroupMember) {memberRole = newRole}
_ -> throwChatError $ CEGroupCantResendInvitation gInfo cName
changeRoleCurrentMems :: User -> Group -> Maybe VersionRoster -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool)
changeRoleCurrentMems user (Group gInfo members) rosterVer memsToChange = case L.nonEmpty memsToChange of
_ -> throwChatError $ CEGroupCantResendInvitation g cName
changeRoleCurrentMems :: User -> Group -> GroupKeys -> Maybe VersionRoster -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool)
changeRoleCurrentMems user (Group gInfo members) gks rosterVer memsToChange = case L.nonEmpty memsToChange of
Nothing -> pure ([], [], [], False)
Just memsToChange' -> do
let mKey m = if isJust rosterVer then MemberKey <$> memberPubKey m else Nothing
events = L.map (\m@GroupMember {memberId} -> XGrpMemRole memberId newRole (mKey m) rosterVer) memsToChange'
recipients = filter memberCurrent members
(msgs_, _gsr) <- sendGroupMessages user gInfo Nothing False recipients False events
(msgs_, _gsr) <- sendGroupMessages user (GIK gInfo gks) Nothing False recipients False events
let signed = any (either (const False) (\SndMessage {signedMsg_} -> isJust signedMsg_)) msgs_
itemsData = zipWith (fmap . sndItemData) memsToChange (L.toList msgs_)
cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) False itemsData Nothing False
@@ -3026,7 +3024,7 @@ processChatCommand cxt nm = \case
APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user ->
withGroupLock "blockForAll" groupId $ do
-- TODO [relays] possible optimization is to read only required members + relays
Group gInfo members <- withFastStore $ \db -> getGroup db cxt user groupId
(Group gInfo members, gks) <- withFastStore $ \db -> getGroupKeys_ db cxt user groupId
when (selfSelected gInfo) $ throwCmdError "can't block/unblock self"
-- TODO [relays] consider sending restriction to all members (remove filtering), as we do in delivery jobs
let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members
@@ -3034,7 +3032,7 @@ processChatCommand cxt nm = \case
when (length memberIds > 1 && anyAdmin) $ throwCmdError "can't block/unblock multiple members when admins selected"
when anyPending $ throwCmdError "can't block/unblock members pending approval"
assertUserGroupRole gInfo $ max GRModerator maxRole
blockMembers user gInfo blockMems remainingMems
blockMembers user (GIK gInfo gks) blockMems remainingMems
where
selfSelected GroupInfo {membership} = elem (groupMemberId' membership) memberIds
selectMembers :: [GroupMember] -> ([GroupMember], [GroupMember], GroupMemberRole, Bool, Bool)
@@ -3047,14 +3045,14 @@ processChatCommand cxt nm = \case
anyPending' = anyPending || memberPending m
in (m : block, remaining, maxRole', anyAdmin', anyPending')
| otherwise = (block, m : remaining, maxRole, anyAdmin, anyPending)
blockMembers :: User -> GroupInfo -> [GroupMember] -> [GroupMember] -> CM ChatResponse
blockMembers user gInfo blockMems remainingMems = case L.nonEmpty blockMems of
blockMembers :: User -> GroupInfoKeys -> [GroupMember] -> [GroupMember] -> CM ChatResponse
blockMembers user g@(GIK gInfo _) blockMems remainingMems = case L.nonEmpty blockMems of
Nothing -> throwCmdError "no members to block/unblock"
Just blockMems' -> do
let mrs = if blockFlag then MRSBlocked else MRSUnrestricted
events = L.map (\GroupMember {memberId} -> XGrpMemRestrict memberId MemberRestrictions {restriction = mrs}) blockMems'
recipients = filter memberCurrent remainingMems
(msgs_, _gsr) <- sendGroupMessages_ user gInfo recipients False events
(msgs_, _gsr) <- sendGroupMessages_ user g recipients False events
let msgSigned = any (either (const False) (\SndMessage {signedMsg_} -> isJust signedMsg_)) msgs_
itemsData = zipWith (fmap . sndItemData) blockMems (L.toList msgs_)
cis_ <- saveSndChatItems user (CDGroupSnd gInfo Nothing) False itemsData Nothing False
@@ -3075,7 +3073,7 @@ processChatCommand cxt nm = \case
APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user ->
withGroupLock "removeMembers" groupId $ do
-- TODO [relays] possible optimization is to read only required members + relays
Group gInfo members <- withFastStore $ \db -> getGroup db cxt user groupId
(Group gInfo members, gks) <- withFastStore $ \db -> getGroupKeys_ db cxt user groupId
let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin, anyPrivilegedRemoved, anyRosterRemoved) = selectMembers gmIds members
gmIds = S.fromList $ L.toList groupMemberIds
memCount = length groupMemberIds
@@ -3088,13 +3086,13 @@ processChatCommand cxt nm = \case
let recipients = filter memberCurrent members
let doBumpRoster = useRelays' gInfo && memberRole' (membership gInfo) == GROwner && anyRosterRemoved
-- roster (excluding the removed members) before the delta, so a relay stores the blob at this version before forwarding the delta
rosterVer <- if doBumpRoster then Just <$> broadcastRoster user gInfo (RDRemoved currentMems) else pure Nothing
(errs2, deleted2, acis2, signed2) <- deleteMemsSend user gInfo Nothing rosterVer recipients currentMems
rosterVer <- if doBumpRoster then Just <$> broadcastRoster user (GIK gInfo gks) (RDRemoved currentMems) else pure Nothing
(errs2, deleted2, acis2, signed2) <- deleteMemsSend user (GIK gInfo gks) Nothing rosterVer recipients currentMems
(errs3, deleted3, acis3, signed3) <-
foldM (\acc m -> deletePendingMember acc user gInfo [m] m) ([], [], [], False) pendingApprvMems
foldM (\acc m -> deletePendingMember acc user (GIK gInfo gks) [m] m) ([], [], [], False) pendingApprvMems
let moderators = filter (\GroupMember {memberRole} -> memberRole >= GRModerator) members
(errs4, deleted4, acis4, signed4) <-
foldM (\acc m -> deletePendingMember acc user gInfo (m : moderators) m) ([], [], [], False) pendingRvwMems
foldM (\acc m -> deletePendingMember acc user (GIK gInfo gks) (m : moderators) m) ([], [], [], False) pendingRvwMems
let acis = acis2 <> acis3 <> acis4
errs = errs1 <> errs2 <> errs3 <> errs4
deleted = deleted1 <> deleted2 <> deleted3 <> deleted4
@@ -3102,7 +3100,7 @@ processChatCommand cxt nm = \case
-- Read group info with updated membersRequireAttention and publicMemberCount
gInfo' <-
if useRelays' gInfo
then updatePublicGroupData user gInfo
then updatePublicGroupData user gInfo gks
else withFastStore $ \db -> getGroupInfo db cxt user groupId
let acis' = map (updateACIGroupInfo gInfo') acis
unless (null acis') $ toView $ CEvtNewChatItems user acis'
@@ -3136,18 +3134,18 @@ processChatCommand cxt nm = \case
delMember db m = do
deleteGroupMember db user m
pure m {memberStatus = GSMemRemoved}
deletePendingMember :: ([ChatError], [GroupMember], [AChatItem], Bool) -> User -> GroupInfo -> [GroupMember] -> GroupMember -> CM ([ChatError], [GroupMember], [AChatItem], Bool)
deletePendingMember :: ([ChatError], [GroupMember], [AChatItem], Bool) -> User -> GroupInfoKeys -> [GroupMember] -> GroupMember -> CM ([ChatError], [GroupMember], [AChatItem], Bool)
deletePendingMember (accErrs, accDeleted, accACIs, accSigned) user gInfo recipients m = do
(m', scopeInfo) <- mkMemberSupportChatInfo m
(errs, deleted, acis, signed) <- deleteMemsSend user gInfo (Just scopeInfo) Nothing recipients [m']
pure (errs <> accErrs, deleted <> accDeleted, acis <> accACIs, accSigned || signed)
deleteMemsSend :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> Maybe VersionRoster -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool)
deleteMemsSend user gInfo chatScopeInfo rosterVer recipients memsToDelete = case L.nonEmpty memsToDelete of
deleteMemsSend :: User -> GroupInfoKeys -> Maybe GroupChatScopeInfo -> Maybe VersionRoster -> [GroupMember] -> [GroupMember] -> CM ([ChatError], [GroupMember], [AChatItem], Bool)
deleteMemsSend user g@(GIK gInfo _) chatScopeInfo rosterVer recipients memsToDelete = case L.nonEmpty memsToDelete of
Nothing -> pure ([], [], [], False)
Just memsToDelete' -> do
let chatScope = toChatScope <$> chatScopeInfo
events = L.map (\GroupMember {memberId} -> XGrpMemDel memberId withMessages rosterVer) memsToDelete'
(msgs_, _gsr) <- sendGroupMessages user gInfo chatScope False recipients False events
(msgs_, _gsr) <- sendGroupMessages user g chatScope False recipients False events
let signed = any (either (const False) (\SndMessage {signedMsg_} -> isJust signedMsg_)) msgs_
itemsData_ = zipWith (fmap . sndItemData) memsToDelete (L.toList msgs_)
skipUnwantedItem = \case
@@ -3184,14 +3182,14 @@ processChatCommand cxt nm = \case
| groupFeatureUserAllowed SGFFullDelete gInfo = deleteGroupMembersCIs user gInfo ms
| otherwise = markGroupMembersCIsDeleted user gInfo ms membership
APILeaveGroup groupId -> withUser $ \user@User {userId} -> do
gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
g@(GIK gInfo@GroupInfo {membership} _) <- withFastStore $ \db -> getGroupInfoKeys db cxt user groupId
filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo
withGroupLock "leaveGroup" groupId $ do
cancelFilesInProgress user filesInfo
msg <-
if useRelays' gInfo && isRelay membership
then leaveChannelRelay gInfo
else leaveGroupSendMsg user gInfo
then leaveChannelRelay g
else leaveGroupSendMsg user g
(gInfo', scopeInfo) <- mkLocalGroupChatScope gInfo
ci <- saveSndChatItem user (CDGroupSnd gInfo' scopeInfo) msg (CISndGroupEvent SGEUserLeft)
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo' scopeInfo) ci]
@@ -3206,9 +3204,9 @@ processChatCommand cxt nm = \case
pure $ CRLeftMemberUser user gInfo' {membership = membership {memberStatus = GSMemLeft}, relayOwnStatus = relayOwnStatus'}
where
-- Relay leaving channel: create delivery job for cursor-based sending and async connection cleanup.
leaveChannelRelay gInfo = do
leaveChannelRelay g@(GIK gInfo _) = do
msg@SndMessage {msgBody, signedMsg_} <-
liftEither . runIdentity =<< lift (createSndMessages $ Identity (GroupId groupId, groupMsgSigning False gInfo XGrpLeave, XGrpLeave))
liftEither . runIdentity =<< lift (createSndMessages $ Identity (GroupId groupId, groupMsgSigning False g XGrpLeave, XGrpLeave))
let body = encodeBatchElement signedMsg_ msgBody
withFastStore' $ \db -> do
deleteGroupDeliveryTasks db gInfo
@@ -3216,9 +3214,9 @@ processChatCommand cxt nm = \case
createMsgDeliveryJob db gInfo (DJSGroup {jobSpec = DJRelayRemoved}) [] body
lift . void $ getDeliveryJobWorker True (groupId, DWSGroup)
pure msg
leaveGroupSendMsg user gInfo = do
leaveGroupSendMsg user g@(GIK gInfo _) = do
(members, recipients) <- getRecipients user gInfo
msg <- sendGroupMessage' user gInfo recipients XGrpLeave
msg <- sendGroupMessage' user g recipients XGrpLeave
deleteMembersConnections' user members True
pure msg
getRecipients user gInfo
@@ -3278,7 +3276,7 @@ processChatCommand cxt nm = \case
ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db cxt user cName
processChatCommand cxt nm $ APIListGroups userId (contactId' <$> ct_) search_
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId
gInfo <- withFastStore $ \db -> getGroupInfoKeys db cxt user groupId
runUpdateGroupProfile user gInfo p' False
UpdateGroupNames gName GroupProfile {displayName, fullName, shortDescr} ->
updateGroupProfileByName gName $ \p -> p {displayName, fullName, shortDescr}
@@ -3289,7 +3287,7 @@ processChatCommand cxt nm = \case
ShowGroupDescription gName -> withUser $ \user ->
CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db cxt user gName)
APISetPublicGroupAccess gId access@PublicGroupAccess {groupDomainClaim = newClaim} -> withUser $ \user -> do
gInfo@GroupInfo {groupProfile = p@GroupProfile {publicGroup}} <- withStore $ \db -> getGroupInfo db cxt user gId
gInfo@(GIK GroupInfo {groupProfile = p@GroupProfile {publicGroup}} _) <- withStore $ \db -> getGroupInfoKeys db cxt user gId
case publicGroup of
Just pg@PublicGroupProfile {groupLink, publicGroupAccess = existingAccess} -> do
let domainChanged = (claimDomain <$> newClaim) /= (claimDomain <$> (existingAccess >>= groupDomainClaim))
@@ -3332,11 +3330,11 @@ processChatCommand cxt nm = \case
gLnk <- withFastStore $ \db -> getGroupLink db user gInfo
pure $ CRGroupLink user gInfo gLnk
APIAddGroupShortLink groupId -> withUser $ \user -> do
(gInfo, gLink) <- withFastStore $ \db -> do
gInfo <- getGroupInfo db cxt user groupId
(g@(GIK gInfo _), gLink) <- withFastStore $ \db -> do
g@(GIK gInfo _) <- getGroupInfoKeys db cxt user groupId
gLink <- getGroupLink db user gInfo
pure (gInfo, gLink)
gLink' <- setGroupLinkData nm user gInfo gLink
pure (g, gLink)
gLink' <- setGroupLinkData nm user g gLink
pure $ CRGroupLink user gInfo gLink'
APICreateMemberContact gId gMemberId -> withUser $ \user -> do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId
@@ -3522,10 +3520,10 @@ processChatCommand cxt nm = \case
void . sendDirectContactMessage user contact $ XFileCancel sharedMsgId
pure $ CRSndFileCancelled user (Just aci) ftm fts
(Just (ChatRef CTGroup groupId scope), Just aci) -> do
(gInfo, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user groupId <*> getSharedMsgIdByFileId db userId fileId
(g@(GIK gInfo _), sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroupInfoKeys db cxt user groupId <*> getSharedMsgIdByFileId db userId fileId
chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope
recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion
void . sendGroupMessage user gInfo scope recipients False $ XFileCancel sharedMsgId
void . sendGroupMessage user g scope recipients False $ XFileCancel sharedMsgId
pure $ CRSndFileCancelled user (Just aci) ftm fts
(Just _, _) -> throwChatError $ CEFileInternal "invalid chat ref for file transfer"
where
@@ -3873,7 +3871,7 @@ processChatCommand cxt nm = \case
-- relay-group joins (only via connectToRelay) carry the target relay member in preparedEntity_;
-- its memberId binds the join signature so a sibling relay can't replay it
relayMemberId_ = case preparedEntity_ of
Just (PCEGroup gInfo m) | useRelays' gInfo -> Just (memberId' m)
Just (PCEGroup (GIK gInfo _) m) | useRelays' gInfo -> Just (memberId' m)
_ -> Nothing
joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} gInfo_ = do
when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection"
@@ -3890,7 +3888,7 @@ processChatCommand cxt nm = \case
xContactId <- mkXContactId xContactId_
-- [incognito] generate profile to send, or use membership profile for relay groups
incognitoProfile_ <- case gInfo_ of
Just (Just gInfo) | useRelays' gInfo -> pure $ ExistingIncognito <$> incognitoMembershipProfile gInfo
Just (Just (GIK gInfo _)) | useRelays' gInfo -> pure $ ExistingIncognito <$> incognitoMembershipProfile gInfo
_ -> if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
let incognitoProfile = fromIncognitoProfile <$> incognitoProfile_
subMode <- chatReadVar subscriptionMode
@@ -3924,8 +3922,8 @@ processChatCommand cxt nm = \case
ct' <- withStore $ \db -> getContact db cxt user contactId
pure $ CRSentInvitationToContact user ct' incognitoProfile
_ -> throwCmdError "contact already has connection"
connectToRelay :: User -> GroupInfo -> ShortLinkContact -> CM (ShortLinkContact, GroupMember, Either ChatError ())
connectToRelay user gInfo relayLink = do
connectToRelay :: User -> GroupInfoKeys -> ShortLinkContact -> CM (ShortLinkContact, GroupMember, Either ChatError ())
connectToRelay user g@(GIK gInfo _) relayLink = do
gVar <- asks random
-- Save relayLink to re-use relay member record on retry (check by relayLink)
relayMember <- withFastStore $ \db -> getCreateRelayForMember db cxt gVar user gInfo relayLink
@@ -3938,7 +3936,7 @@ processChatCommand cxt nm = \case
pure $ MemberId entityId
_ -> throwChatError $ CEException "relay link: no relay link data or entity id"
let relayLinkToConnect = CCLink cReq (Just relayLink)
void $ connectViaContact user (Just $ PCEGroup gInfo (relayMember {memberId = relayMemberId})) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing
void $ connectViaContact user (Just $ PCEGroup g (relayMember {memberId = relayMemberId})) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing
relayMember' <- withFastStore $ \db -> getGroupMember db cxt user (groupId' gInfo) (groupMemberId' relayMember)
pure (relayLink, relayMember', r)
syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM ()
@@ -3971,21 +3969,19 @@ processChatCommand cxt nm = \case
pure (connId, chatV)
mkXContactId :: Maybe XContactId -> CM XContactId
mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure
joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfo) -> Maybe MemberId -> PQSupport -> CM Connection
joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe (Maybe GroupInfoKeys) -> Maybe MemberId -> PQSupport -> CM Connection
joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ gInfo_ relayMemberId_ pqSup = do
-- gInfo_ is Maybe (Maybe GroupInfo), where Just Nothing means "some unknown group", e.g. when joining via link without profile
profileToSend <-
presentUserBadge user incognitoProfile $ case gInfo_ of
Just gInfo_' -> userProfileInGroup' user gInfo_' incognitoProfile
Just gInfo_' -> userProfileInGroup' user ((\(GIK g _) -> g) <$> gInfo_') incognitoProfile
Nothing -> userProfileDirect user incognitoProfile Nothing True
dm <- case gInfo_ of
Just (Just gInfo)
| useRelays' gInfo -> case relayMemberId_ of
Just (Just gInfo@(GIK g gks))
| useRelays' g -> case relayMemberId_ of
Just relayMemberId -> encodeXMemberConnInfo gInfo relayMemberId profileToSend
Nothing -> throwChatError $ CEInternalError "relay group join without target relay memberId"
| otherwise -> do
gInfo' <- createUserMemberKey gInfo
encodeConnInfoPQ pqSup $ XContact profileToSend (groupMemberKey gInfo') (Just xContactId) welcomeSharedMsgId msg_
| otherwise -> encodeConnInfoPQ pqSup $ XContact profileToSend (Just $ groupMemberKey gks) (Just xContactId) welcomeSharedMsgId msg_
_ ->
encodeConnInfoPQ pqSup $ XContact profileToSend Nothing (Just xContactId) welcomeSharedMsgId msg_
subMode <- chatReadVar subscriptionMode
@@ -4100,8 +4096,8 @@ processChatCommand cxt nm = \case
void (sendDirectContactMessage user ct' $ XInfo p Nothing) `catchAllErrors` eToView
lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct'
pure $ CRContactPrefsUpdated user ct ct'
runUpdateGroupProfile :: User -> GroupInfo -> GroupProfile -> Bool -> CM ChatResponse
runUpdateGroupProfile user gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} p'@GroupProfile {displayName = n', image = img', memberAdmission = ma'} domainVerified = do
runUpdateGroupProfile :: User -> GroupInfoKeys -> GroupProfile -> Bool -> CM ChatResponse
runUpdateGroupProfile user (GIK gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} gks) p'@GroupProfile {displayName = n', image = img', memberAdmission = ma'} domainVerified = do
assertUserGroupRole gInfo GROwner
when (n /= n') $ checkValidName n'
checkProfileImageSize img'
@@ -4121,14 +4117,14 @@ processChatCommand cxt nm = \case
withStore $ \db -> getGroupMemberByMemberId db cxt user gInfo' businessId
let p'' = p' {displayName, fullName, shortDescr, image} :: GroupProfile
recipients = filter memberCurrentOrPending oldMs
void $ sendGroupMessage user gInfo' Nothing recipients False (XGrpInfo p'')
void $ sendGroupMessage user (GIK gInfo' gks) Nothing recipients False (XGrpInfo p'')
let ps' = fromMaybe defaultBusinessGroupPrefs $ groupPreferences p'
recipients = filter memberCurrentOrPending newMs
sendGroupMessage user gInfo' Nothing recipients False $ XGrpPrefs ps'
sendGroupMessage user (GIK gInfo' gks) Nothing recipients False $ XGrpPrefs ps'
Nothing -> do
void $ setGroupLinkData' nm user gInfo'
void $ setGroupLinkData' nm user (GIK gInfo' gks)
recipients <- getRecipients
sendGroupMessage user gInfo' Nothing recipients False (XGrpInfo p')
sendGroupMessage user (GIK gInfo' gks) Nothing recipients False (XGrpInfo p')
where
getRecipients
| useRelays' gInfo' = withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo'
@@ -4152,13 +4148,13 @@ processChatCommand cxt nm = \case
when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g)
when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved
unless (memberActive membership) $ throwChatError CEGroupMemberNotActive
delGroupChatItemsForMembers :: User -> GroupInfo -> Maybe GroupChatScopeInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion]
delGroupChatItemsForMembers user gInfo chatScopeInfo ms items = do
delGroupChatItemsForMembers :: User -> GroupInfoKeys -> Maybe GroupChatScopeInfo -> [GroupMember] -> [CChatItem 'CTGroup] -> CM [ChatItemDeletion]
delGroupChatItemsForMembers user g@(GIK gInfo _) chatScopeInfo ms items = do
assertDeletable gInfo items
assertUserGroupRole gInfo GRModerator
let msgMemIds = itemsMsgMemIds gInfo items
-- moderation deletes always sign (attributable; avoids the catch-up-moderator divergence)
signedEvents = L.nonEmpty $ map (\(msgId, memId) -> let evt = XMsgDel msgId memId (toMsgScope gInfo <$> chatScopeInfo) False in (groupMsgSigning True gInfo evt, evt)) msgMemIds
signedEvents = L.nonEmpty $ map (\(msgId, memId) -> let evt = XMsgDel msgId memId (toMsgScope gInfo <$> chatScopeInfo) False in (groupMsgSigning True g evt, evt)) msgMemIds
mapM_ (sendGroupSignedMessages_ gInfo ms) signedEvents
delGroupChatItems user gInfo chatScopeInfo items True
where
@@ -4196,10 +4192,10 @@ processChatCommand cxt nm = \case
updateGroupProfileByName = updateGroupProfileByName_ Nothing
updateGroupProfileByName_ :: Maybe GroupFeature -> GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse
updateGroupProfileByName_ feature_ gName update = withUser $ \user -> do
gInfo@GroupInfo {groupProfile = p} <- withStore $ \db ->
getGroupIdByName db user gName >>= getGroupInfo db cxt user
gInfo@(GIK g@GroupInfo {groupProfile = p} _) <- withStore $ \db ->
getGroupIdByName db user gName >>= getGroupInfoKeys db cxt user
forM_ feature_ $ \feature -> do
let channel = useRelays' gInfo
let channel = useRelays' g
applicable = if channel then groupFeatureInChannel feature else groupFeatureInRegularGroup feature
unless applicable $
throwCmdError $ T.unpack (groupFeatureNameText feature) <> " is not available in " <> (if channel then "channels" else "groups")
@@ -4257,29 +4253,30 @@ processChatCommand cxt nm = \case
groupId <- getGroupIdByName db user gName
groupMemberId <- getGroupMemberIdByName db user groupId groupMemberName
pure (groupId, groupMemberId)
newGroup :: User -> IncognitoEnabled -> GroupProfile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> CM GroupInfo
newGroup user incognito gProfile@GroupProfile {displayName, image, memberAdmission} useRelays memberId groupKeys_ publicMemberCount_ = do
newGroup :: User -> IncognitoEnabled -> GroupProfile -> MemberId -> GroupKeys -> Maybe Int64 -> CM GroupInfo
newGroup user incognito gProfile@GroupProfile {displayName, image, memberAdmission, publicGroup} memberId groupKeys publicMemberCount_ = do
checkValidName displayName
checkProfileImageSize image
checkGroupProfileSize gProfile
when (useRelays && isJust (memberAdmission >>= review)) $ throwCmdError "Admission review is not supported in channels"
when (isPublicGroup groupKeys && isJust (memberAdmission >>= review)) $ throwCmdError "Admission review is not supported in channels"
when (not (isPublicGroup groupKeys) && isJust publicGroup) $ throwCmdError "publicGroup is not allowed in groups"
-- [incognito] generate incognito profile for group membership
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile useRelays memberId groupKeys_ publicMemberCount_
withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile memberId groupKeys publicMemberCount_
createNewGroupItems :: User -> GroupInfo -> CM ()
createNewGroupItems user gInfo = do
let cd = CDGroupSnd gInfo Nothing
createInternalChatItem user cd CIChatBanner (Just epochStart)
createInternalChatItem user cd (CISndGroupE2EEInfo $ e2eInfoGroup gInfo) Nothing
createGroupFeatureItems user cd CISndGroupFeature gInfo
sendGrpInvitation :: User -> Contact -> GroupInfo -> GroupMember -> ConnReqInvitation -> CM ()
sendGrpInvitation user ct@Contact {contactId, localDisplayName} gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} m@GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
sendGrpInvitation :: User -> Contact -> GroupInfoKeys -> GroupMember -> ConnReqInvitation -> CM ()
sendGrpInvitation user ct@Contact {contactId, localDisplayName} (GIK gInfo@GroupInfo {groupId, groupProfile, membership, businessChat} gks) m@GroupMember {groupMemberId, memberId, memberRole = memRole} cReq = do
let currentMemCount = fromIntegral $ currentMembers $ groupSummary gInfo
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
groupInv =
GroupInvitation
{ fromMember = MemberIdRole userMemberId userRole,
fromMemberKey = groupMemberKey gInfo,
fromMemberKey = Just $ groupMemberKey gks,
invitedMember = MemberIdRole memberId memRole,
connRequest = cReq,
groupProfile,
@@ -4826,19 +4823,17 @@ processChatCommand cxt nm = \case
quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True)
quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False)
quoteData _ = throwError SEInvalidQuote
sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> Bool -> Maybe Int -> Bool -> NonEmpty ComposedMessageReq -> CM ChatResponse
sendGroupContentMessages user gInfo scope showGroupAsSender live itemTTL sign cmrs = do
sendGroupContentMessages :: User -> GroupInfoKeys -> Maybe GroupChatScope -> ShowGroupAsSender -> Bool -> Maybe Int -> Bool -> NonEmpty ComposedMessageReq -> CM ChatResponse
sendGroupContentMessages user gInfo@(GIK g _) scope showGroupAsSender live itemTTL sign cmrs = do
assertMultiSendable live cmrs
chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope
-- the member key is created before the send, so that signatures and file badge proofs assert the same key
gInfo' <- createUserMemberKey gInfo
recipients <- getGroupRecipients cxt user gInfo' chatScopeInfo modsCompatVersion
sendGroupContentMessages_ user gInfo' scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs
recipients <- getGroupRecipients cxt user g chatScopeInfo modsCompatVersion
sendGroupContentMessages_ user gInfo scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs
where
hasReport = any (\(ComposedMessage {msgContent}, _, _, _) -> isReport msgContent) cmrs
modsCompatVersion = if hasReport then contentReportsVersion else groupKnockingVersion
sendGroupContentMessages_ :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> Maybe GroupChatScopeInfo -> [GroupMember] -> Bool -> Maybe Int -> Bool -> NonEmpty ComposedMessageReq -> CM ChatResponse
sendGroupContentMessages_ user gInfo@GroupInfo {groupId, membership} scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs = do
sendGroupContentMessages_ :: User -> GroupInfoKeys -> Maybe GroupChatScope -> ShowGroupAsSender -> Maybe GroupChatScopeInfo -> [GroupMember] -> Bool -> Maybe Int -> Bool -> NonEmpty ComposedMessageReq -> CM ChatResponse
sendGroupContentMessages_ user g@(GIK gInfo@GroupInfo {groupId, membership} _) scope showGroupAsSender chatScopeInfo recipients live itemTTL sign cmrs = do
forM_ allowedRole $ assertUserGroupRole gInfo
assertGroupContentAllowed
processComposedMessages
@@ -4868,7 +4863,7 @@ processChatCommand cxt nm = \case
(fInvs_, ciFiles_) <- L.unzip <$> setupSndFileTransfers (length recipients)
timed_ <- sndGroupCITimed live gInfo itemTTL
(chatMsgEvents, quotedItems_) <- L.unzip <$> prepareMsgs (L.zip cmrs fInvs_) timed_
(msgs_, gsr) <- sendGroupMessages user gInfo Nothing showGroupAsSender recipients signMsgs chatMsgEvents
(msgs_, gsr) <- sendGroupMessages user g Nothing showGroupAsSender recipients signMsgs chatMsgEvents
let itemsData = prepareSndItemsData (L.toList cmrs) (L.toList ciFiles_) (L.toList quotedItems_) (L.toList msgs_)
cis_ <- saveSndChatItems user (CDGroupSnd gInfo chatScopeInfo) showGroupAsSender itemsData timed_ live
when (length cis_ /= length cmrs) $ logError "sendGroupContentMessages: cmrs and cis_ length mismatch"
@@ -4992,12 +4987,12 @@ processChatCommand cxt nm = \case
where
getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect))
getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user ctId itemId
getCommandGroupChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (GroupInfo, [CChatItem 'CTGroup])
getCommandGroupChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (GroupInfoKeys, [CChatItem 'CTGroup])
getCommandGroupChatItems user gId itemIds = do
gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId
g@(GIK gInfo _) <- withFastStore $ \db -> getGroupInfoKeys db cxt user gId
(errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db gInfo) (L.toList itemIds))
unless (null errs) $ toView $ CEvtChatErrors errs
pure (gInfo, items)
pure (g, items)
where
getGroupCI :: DB.Connection -> GroupInfo -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup))
getGroupCI db gInfo itemId = runExceptT . withExceptT ChatErrorStore $ getGroupCIWithReactions db user gInfo itemId
@@ -5206,7 +5201,7 @@ presentUserBadgeToContacts user'@User {userId, profile = LocalProfile {localBadg
Just User {userId = activeId} | activeId == userId -> Just user'
active_ -> active_
lift $ withAgent' $ \a -> setUserEntitlement a (aUserId user') (badgeServerCredential localBadge)
cxt <- asks $ mkStoreCxt . config
cxt <- chatStoreCxt
contacts <- withFastStore' $ \db -> getUserContacts db cxt user'
withChatLock "presentUserBadge" $ forM_ contacts $ \ct ->
case contactSendConn_ ct of
@@ -5839,8 +5834,8 @@ runRelayGroupLinkChecks user = do
where
checkRelayServedGroups = do
cxt <- chatStoreCxt
relayGroups <- withStore' $ \db -> getRelayServedGroups db cxt user
forM_ relayGroups $ \gInfo@GroupInfo {groupProfile = gp} -> flip catchAllErrors eToView $ do
relayGroups <- withStore $ \db -> getRelayServedGroups db cxt user
forM_ relayGroups $ \g@(GIK gInfo@GroupInfo {groupProfile = gp} _) -> flip catchAllErrors eToView $ do
case publicGroup gp of
Just PublicGroupProfile {groupLink = sLnk} -> do
(_, ContactLinkData _ UserContactData {relays = relayLinks}, _) <-
@@ -5856,7 +5851,7 @@ runRelayGroupLinkChecks user = do
else void $ withStore' $ \db -> updateRelayOwnStatusFromTo db gInfo RSActive RSInactive
_ -> pure ()
_ -> pure ()
sendRelayCapIfNeeded user gInfo
sendRelayCapIfNeeded user g
checkRelayInactiveGroups = do
cxt <- chatStoreCxt
ttl <- asks (relayInactiveTTL . config)
+103 -118
View File
@@ -470,9 +470,9 @@ sndBadgeProof_ User {profile = LocalProfile {localBadge}} ph = case localBadge o
_ -> pure Nothing
sndGroupChatBinding :: GroupInfo -> ShowGroupAsSender -> Maybe ByteString
sndGroupChatBinding GroupInfo {groupKeys, membership = GroupMember {memberId}} asGroup
| asGroup = (\PublicGroupKeys {publicGroupId} -> encodeChatBinding CBChannel $ smpEncode publicGroupId) <$> (groupKeys >>= publicGroupKeys)
| otherwise = (\GroupKeys {memberPrivKey} -> encodeChatBinding CBGroup $ groupBindingData groupKeys memberId (C.publicKey memberPrivKey)) <$> groupKeys
sndGroupChatBinding gInfo@GroupInfo {membership = GroupMember {memberId, memberPubKey}} asGroup
| asGroup = (\PublicGroupProfile {publicGroupId} -> encodeChatBinding CBChannel $ smpEncode publicGroupId) <$> publicGroup' gInfo
| otherwise = (\k -> encodeChatBinding CBGroup $ groupBindingData gInfo memberId k) <$> memberPubKey
cryptoFileDigest :: CryptoFile -> CM FD.FileDigest
cryptoFileDigest (CryptoFile filePath cfArgs) = do
@@ -1020,11 +1020,11 @@ acceptContactRequestAsync
agentAcceptContactAsync cmdId acId True cReqInvId (XInfo profileToSend Nothing) cReqPQSup subMode
pure ct'
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 -> Int64 -> GroupInfoKeys -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> Maybe GroupMember -> CM GroupMember
acceptGroupJoinRequestAsync
user@User {userId}
uclId
gInfo@GroupInfo {groupProfile, membership, businessChat}
(GIK gInfo@GroupInfo {groupProfile, membership, businessChat} gks)
cReqInvId
cReqChatVRange
cReqProfile
@@ -1058,7 +1058,7 @@ acceptGroupJoinRequestAsync
GroupLinkInvitation
{ fromMember = MemberIdRole userMemberId userRole,
fromMemberName = displayName,
fromMemberKey = groupMemberKey gInfo,
fromMemberKey = Just $ groupMemberKey gks,
invitedMember = MemberIdRole memberId gLinkMemRole,
groupProfile,
accepted = Just gAccepted,
@@ -1106,11 +1106,11 @@ acceptGroupJoinSendRejectAsync
agentAcceptContactAsync cmdId acId False cReqInvId msg PQSupportOff subMode
pure m
acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember)
acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfoKeys -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember)
acceptBusinessJoinRequestAsync
user
uclId
gInfo@GroupInfo {membership = GroupMember {memberRole = userRole, memberId = userMemberId}}
(GIK gInfo@GroupInfo {membership = GroupMember {memberRole = userRole, memberId = userMemberId}} gks)
clientMember@GroupMember {groupMemberId, memberId}
UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId} = do
cxt <- chatStoreCxt
@@ -1122,7 +1122,7 @@ acceptBusinessJoinRequestAsync
GroupLinkInvitation
{ fromMember = MemberIdRole userMemberId userRole,
fromMemberName = displayName,
fromMemberKey = groupMemberKey gInfo,
fromMemberKey = Just $ groupMemberKey gks,
invitedMember = MemberIdRole memberId GRMember,
groupProfile = businessGroupProfile userProfile groupPreferences,
accepted = Just GAAccepted,
@@ -1197,15 +1197,15 @@ businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile
businessGroupProfile Profile {displayName, fullName, shortDescr, description, image} groupPreferences =
GroupProfile {displayName, fullName, description, shortDescr, image, publicGroup = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing}
introduceToModerators :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM ()
introduceToModerators cxt user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do
introduceToModerators :: StoreCxt -> User -> GroupInfoKeys -> GroupMember -> CM ()
introduceToModerators cxt user gInfo@(GIK g@GroupInfo {groupId} _) m@GroupMember {memberRole, memberId} = do
forM_ (memberConn m) $ \mConn -> do
let msg =
if maxVersion (memberChatVRange m) >= groupKnockingVersion
then XGrpLinkAcpt GAPendingReview memberRole memberId
else XMsgNew $ mcSimple (MCText pendingReviewMessage)
void $ sendDirectMemberMessage mConn msg groupId
modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo
modMs <- withStore' $ \db -> getGroupModerators db cxt user g
let rcpModMs = filter shouldIntroduceToMod modMs
introduceMember user gInfo m rcpModMs (Just $ MSMember $ memberId' m)
where
@@ -1215,15 +1215,15 @@ introduceToModerators cxt user gInfo@GroupInfo {groupId} m@GroupMember {memberRo
&& groupMemberId' mem /= groupMemberId' m
&& maxVersion (memberChatVRange mem) >= groupKnockingVersion
introduceToAll :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM ()
introduceToAll cxt user gInfo m = do
(members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db cxt user gInfo) (getMemberRelationsVector db m)
introduceToAll :: StoreCxt -> User -> GroupInfoKeys -> GroupMember -> CM ()
introduceToAll cxt user gInfo@(GIK g _) m = do
(members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db cxt user g) (getMemberRelationsVector db m)
let recipients = filter (shouldIntroduce m vector) members
introduceMember user gInfo m recipients Nothing
introduceToRemaining :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM ()
introduceToRemaining cxt user gInfo m = do
(members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db cxt user gInfo) (getMemberRelationsVector db m)
introduceToRemaining :: StoreCxt -> User -> GroupInfoKeys -> GroupMember -> CM ()
introduceToRemaining cxt user gInfo@(GIK g _) m = do
(members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db cxt user g) (getMemberRelationsVector db m)
let recipients = filter (shouldIntroduce m vector) members
introduceMember user gInfo m recipients Nothing
@@ -1233,17 +1233,17 @@ shouldIntroduce m vec mem =
&& groupMemberId' mem /= groupMemberId' m
&& getRelation (indexInGroup mem) vec == MRNew
introduceMember :: User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM ()
introduceMember :: User -> GroupInfoKeys -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM ()
introduceMember _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active"
introduceMember user gInfo toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do
void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo gInfo toMember) msgScope
introduceMember user gInfo@(GIK g _) toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do
void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo g toMember) msgScope
sendIntroductions introduceToMembers
where
sendIntroductions reMembers = do
updateToMemberVector reMembers
updateReMembersVectors reMembers
shuffledReMembers <- liftIO $ shuffleMembers reMembers
let events = map (memberIntroEvt gInfo) shuffledReMembers
let events = map (memberIntroEvt g) shuffledReMembers
forM_ (L.nonEmpty events) $ \events' ->
sendGroupMemberMessages user gInfo conn events'
updateToMemberVector :: [GroupMember] -> CM ()
@@ -1272,11 +1272,11 @@ memberIntroEvt gInfo reMember =
-- 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 =
serveRoster :: User -> GroupInfoKeys -> GroupMember -> CM ()
serveRoster user gInfo@(GIK g _) member =
when (member `supportsVersion` groupRosterVersion) $ do
cxt <- chatStoreCxt
withStore' (\db -> getStoredGroupRoster db gInfo) >>= \case
withStore' (\db -> getStoredGroupRoster db g) >>= \case
Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}, blob_, storedVer_) ->
case J.eitherDecodeStrict' signedBody :: Either String (ChatMessage 'Json) of
Left e -> logError $ "serveRoster: cannot decode saved roster message: " <> tshow e
@@ -1297,24 +1297,24 @@ serveRoster user gInfo member =
-- 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 :: StoreCxt -> User -> GroupInfoKeys -> GroupMember -> CM ()
introduceInChannel _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active"
introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do
introduceInChannel cxt user g@(GIK gInfo _) subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do
(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
void $ sendGroupMessage' user g modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing
withStore' $ \db ->
setMemberVectorNewRelations db subscriber [(indexInGroup m, (IDSubjectIntroduced, MRIntroduced)) | m <- modMs]
-- 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
serveRoster user g 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
sendGroupMemberMessages user g conn evts
userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile
userProfileInGroup user g = userProfileInGroup' user (Just g)
@@ -1558,29 +1558,29 @@ splitFileDescr partSize lastSize rfdText = splitParts 1 rfdText
then fileDescr :| []
else fileDescr <| splitParts (partNo + 1) rest
setGroupLinkData' :: NetworkRequestMode -> User -> GroupInfo -> CM (Maybe GroupLink)
setGroupLinkData' nm user gInfo =
withFastStore' (\db -> runExceptT $ getGroupLink db user gInfo) >>= \case
setGroupLinkData' :: NetworkRequestMode -> User -> GroupInfoKeys -> CM (Maybe GroupLink)
setGroupLinkData' nm user gInfo@(GIK g _) =
withFastStore' (\db -> runExceptT $ getGroupLink db user g) >>= \case
Right gLink@GroupLink {shortLinkDataSet}
| shortLinkDataSet -> Just <$> setGroupLinkData nm user gInfo gLink
_ -> pure Nothing
setGroupLinkData :: NetworkRequestMode -> User -> GroupInfo -> GroupLink -> CM GroupLink
setGroupLinkData nm user gInfo gLink = do
setGroupLinkData :: NetworkRequestMode -> User -> GroupInfoKeys -> GroupLink -> CM GroupLink
setGroupLinkData nm user g@(GIK gInfo _) gLink = do
cxt <- chatStoreCxt
(conn, groupRelays) <- withFastStore $ \db ->
(,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo)
let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays
let (userLinkData, crClientData) = groupLinkData g 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) False Nothing)
withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk
setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM ()
setGroupLinkDataAsync user gInfo gLink = do
setGroupLinkDataAsync :: User -> GroupInfoKeys -> GroupLink -> CM ()
setGroupLinkDataAsync user g@(GIK gInfo _) gLink = do
cxt <- chatStoreCxt
(conn, groupRelays) <- withStore $ \db ->
(,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getPublishableGroupRelays db cxt user gInfo)
let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays
let (userLinkData, crClientData) = groupLinkData g gLink groupRelays
setAgentConnShortLinkAsync user conn userLinkData (Just crClientData)
connectToRelayAsync :: User -> GroupInfo -> ShortLinkContact -> CM ()
@@ -1595,15 +1595,15 @@ connectToRelayAsync user gInfo relayLink = do
newConnIds <- getAgentConnShortLinkAsync user CFGetRelayDataJoin Nothing relayLink
withFastStore' $ \db -> createRelayMemberConnectionAsync db user gInfo relayMember relayLink newConnIds subMode
updatePublicGroupData :: User -> GroupInfo -> CM GroupInfo
updatePublicGroupData user gInfo
updatePublicGroupData :: User -> GroupInfo -> GroupKeys -> CM GroupInfo
updatePublicGroupData user gInfo gks
| useRelays' gInfo && memberRole' (membership gInfo) == GROwner = do
cxt <- chatStoreCxt
(gInfo', gLink) <- withStore $ \db -> do
gInfo' <- updatePublicMemberCount db cxt user gInfo
gLink <- getGroupLink db user gInfo'
pure (gInfo', gLink)
setGroupLinkDataAsync user gInfo' gLink
setGroupLinkDataAsync user (GIK gInfo' gks) gLink
pure gInfo'
| useRelays' gInfo && isRelay (membership gInfo) = do
cxt <- chatStoreCxt
@@ -1647,14 +1647,14 @@ updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {conta
verifyChanged = contactDomainVerified /= Just True || claimChanged
-- TODO [relays] owner: set owners on updating link data (multi-owner)
groupLinkData :: GroupInfo -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData)
groupLinkData gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}, membership = GroupMember {memberId}, groupKeys} GroupLink {groupLinkId} groupRelays =
groupLinkData :: GroupInfoKeys -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData)
groupLinkData (GIK gInfo@GroupInfo {groupProfile, groupSummary = GroupSummary {publicMemberCount}, membership = GroupMember {memberId}} gks) GroupLink {groupLinkId} groupRelays =
let direct = not $ useRelays' gInfo
relays = mapMaybe (\GroupRelay {relayLink} -> relayLink) groupRelays
publicGroupData_ = PublicGroupData <$> publicMemberCount
userData = encodeShortLinkData $ GroupShortLinkData {groupProfile, publicGroupData = publicGroupData_}
owners = case groupKeys of
Just GroupKeys {publicGroupKeys = Just PublicGroupKeys {groupRootKey = GRKPrivate rootPrivKey}, memberPrivKey} ->
owners = case gks of
GKPublicGroup {groupRootKey = GRKPrivate rootPrivKey, memberPrivKey} ->
let ownerId = unMemberId memberId
ownerKey = C.publicKey memberPrivKey
authOwnerSig = C.sign' rootPrivKey (ownerId <> C.encodePubKey ownerKey)
@@ -2308,18 +2308,19 @@ createSndMessages idsEvents = do
encodeMessage sharedMsgId =
encodeChatMessage maxEncodedMsgLength ChatMessage {chatVRange = vr, msgId = Just sharedMsgId, chatMsgEvent = evnt}
groupMsgSigning :: Bool -> GroupInfo -> ChatMsgEvent e -> Maybe MsgSigning
groupMsgSigning sign GroupInfo {membership = GroupMember {memberId}, groupKeys} evt = case groupKeys of
Just gks@GroupKeys {memberPrivKey} | shouldSign -> Just $ MsgSigning CBGroup bindingData KRMember memberPrivKey
where
tag = toCMEventTag evt
shouldSign = requiresSignature tag || (sign && signableContent tag)
bindingData = groupBindingData (Just gks) memberId (C.publicKey memberPrivKey)
_ -> Nothing
groupBindingData :: Maybe GroupKeys -> MemberId -> C.PublicKeyEd25519 -> ByteString
groupBindingData gks memberId memberKey = case gks >>= publicGroupKeys of
Just PublicGroupKeys {publicGroupId} -> smpEncode (publicGroupId, memberId)
groupMsgSigning :: Bool -> GroupInfoKeys -> ChatMsgEvent e -> Maybe MsgSigning
groupMsgSigning sign (GIK gInfo@GroupInfo {membership = GroupMember {memberId}} gks) evt
| shouldSign = Just $ MsgSigning CBGroup bindingData KRMember memberPrivKey'
| otherwise = Nothing
where
memberPrivKey' = memberPrivKey gks
tag = toCMEventTag evt
shouldSign = requiresSignature tag || (sign && signableContent tag)
bindingData = groupBindingData gInfo memberId (C.publicKey memberPrivKey')
groupBindingData :: GroupInfo -> MemberId -> C.PublicKeyEd25519 -> ByteString
groupBindingData gInfo memberId memberKey = case publicGroup' gInfo of
Just PublicGroupProfile {publicGroupId} -> smpEncode (publicGroupId, memberId)
Nothing -> smpEncode (memberId, memberKey)
type HistoryFile = (FileInvitation, RcvFileDescrText, Maybe UTCTime, Maybe BadgeProof)
@@ -2330,11 +2331,11 @@ directChatBinding ct =
encodeChatBinding CBDirect <$> withAgent (`getConnectionRatchetAdHash` aConnId conn)
rcvGroupChatBinding :: GroupInfo -> Maybe GroupMember -> ShowGroupAsSender -> Maybe BadgeProof -> Maybe ByteString
rcvGroupChatBinding GroupInfo {groupKeys} m_ asGroup badge_ =
case (groupKeys >>= publicGroupKeys, asGroup, m_) of
(Just PublicGroupKeys {publicGroupId}, True, _) ->
rcvGroupChatBinding gInfo m_ asGroup badge_ =
case (publicGroup' gInfo, asGroup, m_) of
(Just PublicGroupProfile {publicGroupId}, True, _) ->
Just $ encodeChatBinding CBChannel $ smpEncode publicGroupId
(Just PublicGroupKeys {publicGroupId}, False, Just GroupMember {memberId}) ->
(Just PublicGroupProfile {publicGroupId}, False, Just GroupMember {memberId}) ->
Just $ encodeChatBinding CBGroup $ smpEncode (publicGroupId, memberId)
(Nothing, False, Just GroupMember {memberId, memberPubKey}) ->
(\k -> encodeChatBinding CBGroup $ smpEncode (memberId, k)) <$> (memberPubKey <|> proofMemberKey memberId badge_)
@@ -2387,21 +2388,13 @@ rcvFileProhibited binding_ FileInvitation {fileSize, fileBadge} = do
then Nothing
else Just FileProhibited {maxSize, badgeStatus = Just st}
createUserMemberKey :: GroupInfo -> CM GroupInfo
createUserMemberKey gInfo@GroupInfo {groupId, membership, groupKeys}
| useRelays' gInfo || isJust groupKeys = pure gInfo
| otherwise = do
(_, memberPrivKey) <- atomically . C.generateKeyPair =<< asks random
withStore' $ \db -> setUserMemberKey db groupId (groupMemberId' membership) memberPrivKey
pure gInfo {groupKeys = Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey}}
groupMemberKey :: GroupKeys -> MemberKey
groupMemberKey gks = MemberKey $ C.publicKey $ memberPrivKey gks
groupMemberKey :: GroupInfo -> Maybe MemberKey
groupMemberKey GroupInfo {groupKeys} = MemberKey . C.publicKey . memberPrivKey <$> groupKeys
sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> GroupInfo -> Connection -> NonEmpty (ChatMsgEvent e) -> CM ()
sendGroupMemberMessages user gInfo@GroupInfo {groupId} conn events = do
sendGroupMemberMessages :: forall e. MsgEncodingI e => User -> GroupInfoKeys -> Connection -> NonEmpty (ChatMsgEvent e) -> CM ()
sendGroupMemberMessages user g@(GIK 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
let idsEvts = L.map (\evt -> (GroupId groupId, groupMsgSigning False g evt, evt)) events
(errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts
unless (null errs) $ toView $ CEvtChatErrors errs
forM_ (L.nonEmpty msgs) $ \msgs' ->
@@ -2468,15 +2461,13 @@ encodeSignedConnInfo signing chatMsgEvent = do
-- 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 gks@GroupKeys {memberPrivKey} ->
let xMemberEvt = XMember profileToSend memberId (MemberKey $ C.publicKey memberPrivKey) (Just relayMemberId)
bindingData = groupBindingData (Just gks) memberId (C.publicKey memberPrivKey)
signing = MsgSigning CBGroup bindingData KRMember memberPrivKey
in encodeSignedConnInfo signing xMemberEvt
Nothing -> throwChatError $ CEInternalError "no group keys for channel membership"
encodeXMemberConnInfo :: GroupInfoKeys -> MemberId -> Profile -> CM ByteString
encodeXMemberConnInfo (GIK gInfo@GroupInfo {membership = GroupMember {memberId}} gks) relayMemberId profileToSend =
let memberPrivKey' = memberPrivKey gks
xMemberEvt = XMember profileToSend memberId (MemberKey $ C.publicKey memberPrivKey') (Just relayMemberId)
bindingData = groupBindingData gInfo memberId (C.publicKey memberPrivKey')
signing = MsgSigning CBGroup bindingData KRMember memberPrivKey'
in encodeSignedConnInfo signing xMemberEvt
deliverMessage :: Connection -> CMEventTag e -> MsgBody -> MessageId -> CM (Int64, PQEncryption)
deliverMessage conn cmEventTag msgBody msgId = do
@@ -2539,13 +2530,13 @@ deliverMessagesB msgReqs = do
where
updatePQ = updateConnPQSndEnabled db connId pqSndEnabled'
sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> [GroupMember] -> Bool -> ChatMsgEvent e -> CM SndMessage
sendGroupMessage :: MsgEncodingI e => User -> GroupInfoKeys -> Maybe GroupChatScope -> [GroupMember] -> Bool -> ChatMsgEvent e -> CM SndMessage
sendGroupMessage user gInfo gcScope members sign chatMsgEvent = do
sendGroupMessages user gInfo gcScope False members sign (chatMsgEvent :| []) >>= \case
((Right msg) :| [], _) -> pure msg
_ -> throwChatError $ CEInternalError "sendGroupMessage: expected 1 message"
sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage
sendGroupMessage' :: MsgEncodingI e => User -> GroupInfoKeys -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage
sendGroupMessage' user gInfo members chatMsgEvent =
sendGroupMessages_ user gInfo members False (chatMsgEvent :| []) >>= \case
((Right msg) :| [], _) -> pure msg
@@ -2571,8 +2562,8 @@ applyRosterDelta delta current = case delta of
-- advances past a version the owner hasn't recorded), then broadcast the matching blob with the change projected
-- onto the served roster (so it excludes demoted/removed members). Returns the reserved version for the delta
-- that follows. The blob send is best-effort - a failed send heals on the next change or on resume.
broadcastRoster :: User -> GroupInfo -> RosterDelta -> CM VersionRoster
broadcastRoster user gInfo delta = do
broadcastRoster :: User -> GroupInfoKeys -> RosterDelta -> CM VersionRoster
broadcastRoster user g@(GIK gInfo _) delta = do
let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo)
withStore' $ \db -> setGroupRosterVersion db gInfo rosterVer
sendRosterBlob rosterVer `catchAllErrors` eToView
@@ -2583,18 +2574,18 @@ broadcastRoster user gInfo delta = do
(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 $ applyRosterDelta delta rosterMems)
sendRoster user g (L.toList relays') rosterVer (buildGroupRoster $ applyRosterDelta delta 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 =
sendGroupRosterToRelay :: User -> GroupInfoKeys -> GroupMember -> CM ()
sendGroupRosterToRelay user g@(GIK gInfo _) relayMember =
forM_ (rosterVersion gInfo) $ \rosterVer -> do
cxt <- chatStoreCxt
rosterMems <- withStore' $ \db -> getGroupRosterMembers db cxt user gInfo
sendRoster user gInfo [relayMember] rosterVer (buildGroupRoster rosterMems)
sendRoster user g [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 -> GroupInfoKeys -> [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}
@@ -2602,7 +2593,7 @@ sendRoster user gInfo members rosterVer roster = do
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 -> GroupInfoKeys -> [GroupMember] -> SharedMsgId -> ByteString -> CM ()
sendInlineBlobChunks user gInfo members sharedMsgId blob = do
chSize <- fromIntegral <$> asks (fileChunkSize . config)
go chSize 1 blob
@@ -2615,8 +2606,8 @@ sendInlineBlobChunks user gInfo members sharedMsgId blob = do
-- 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).
sendRelayCapIfNeeded :: User -> GroupInfo -> CM ()
sendRelayCapIfNeeded user gInfo = do
sendRelayCapIfNeeded :: User -> GroupInfoKeys -> CM ()
sendRelayCapIfNeeded user g@(GIK gInfo _) = do
ChatConfig {webPreviewConfig} <- asks config
let currentWebDomain = (\WebPreviewConfig {webDomain} -> webDomain) <$> webPreviewConfig
sentWebDomain <- withStore' (`getRelaySentWebDomain` gInfo)
@@ -2625,24 +2616,22 @@ sendRelayCapIfNeeded user gInfo = do
owners <- withStore' $ \db -> getGroupOwners db cxt user gInfo
let capableOwners = filter (\m -> memberCurrent m && m `supportsVersion` relayWebCapVersion) owners
unless (null capableOwners) $ do
void $ sendGroupMessage' user gInfo capableOwners (XGrpRelayCap RelayCapabilities {webDomain = currentWebDomain})
void $ sendGroupMessage' user g capableOwners (XGrpRelayCap RelayCapabilities {webDomain = currentWebDomain})
withStore' $ \db -> updateRelaySentWebDomain db gInfo currentWebDomain
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
gInfo <- createUserMemberKey gInfo'
sendGroupMessages :: MsgEncodingI e => User -> GroupInfoKeys -> 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
-- 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
gInfo <- createUserMemberKey gInfo'
sendGroupSignedMessages :: MsgEncodingI e => User -> GroupInfoKeys -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> NonEmpty (Maybe MsgSigning, ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
sendGroupSignedMessages user gInfo@(GIK g _) scope asGroup members signedEvents = do
sendGroupProfileUpdate user gInfo scope asGroup members
sendGroupSignedMessages_ gInfo members signedEvents
sendGroupSignedMessages_ g members signedEvents
sendGroupProfileUpdate :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> CM ()
sendGroupProfileUpdate user gInfo scope asGroup members =
sendGroupProfileUpdate :: User -> GroupInfoKeys -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> CM ()
sendGroupProfileUpdate user g@(GIK gInfo gks) scope asGroup members =
-- TODO [knocking] send current profile to pending member after approval?
when shouldSendProfileUpdate $
sendProfileUpdate `catchAllErrors` eToView
@@ -2661,7 +2650,7 @@ sendGroupProfileUpdate user gInfo scope asGroup members =
sendProfileUpdate = do
-- shouldSendProfileUpdate excludes incognito membership, so the badge is presented
profileUpdate <- presentUserBadge user Nothing $ redactedMemberProfile gInfo (membership gInfo) $ fromLocalProfile p
void $ sendGroupMessage' user gInfo members $ XInfo profileUpdate (groupMemberKey gInfo)
void $ sendGroupMessage' user g members $ XInfo profileUpdate (Just $ groupMemberKey gks)
currentTs <- liftIO getCurrentTime
withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs
@@ -2671,9 +2660,9 @@ data GroupSndResult = GroupSndResult
forwarded :: [GroupMember]
}
sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> Bool -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
sendGroupMessages_ _user gInfo recipientMembers sign events =
sendGroupSignedMessages_ gInfo recipientMembers $ L.map (\evt -> (groupMsgSigning sign gInfo evt, evt)) events
sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfoKeys -> [GroupMember] -> Bool -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
sendGroupMessages_ _user gInfo@(GIK g _) recipientMembers sign events =
sendGroupSignedMessages_ g recipientMembers $ L.map (\evt -> (groupMsgSigning sign gInfo evt, evt)) events
sendGroupSignedMessages_ :: MsgEncodingI e => GroupInfo -> [GroupMember] -> NonEmpty (Maybe MsgSigning, ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
sendGroupSignedMessages_ gInfo@GroupInfo {groupId} recipientMembers signedEvents = do
@@ -3046,10 +3035,10 @@ joinAgentConnectionAsync :: CommandId -> Bool -> ConnId -> Bool -> ConnectionReq
joinAgentConnectionAsync cmdId updateConn connId enableNtfs cReqUri cInfo subMode =
withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) updateConn connId enableNtfs cReqUri cInfo PQSupportOff subMode
allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> Maybe GroupInfo -> ChatMsgEvent e -> CM ()
allowAgentConnectionAsync :: MsgEncodingI e => User -> Connection -> ConfirmationId -> Maybe GroupInfoKeys -> ChatMsgEvent e -> CM ()
allowAgentConnectionAsync user conn@Connection {pqSupport} confId gInfo_ msg = do
let signing_ = case gInfo_ of
Just gInfo | useRelays' gInfo || maxVersion (peerChatVRange conn) >= relayWebCapVersion -> groupMsgSigning False gInfo msg
Just gInfo@(GIK g _) | useRelays' g || maxVersion (peerChatVRange conn) >= relayWebCapVersion -> groupMsgSigning False gInfo msg
_ -> Nothing
dm <- case signing_ of
Just signing -> encodeSignedConnInfo signing msg
@@ -3292,7 +3281,7 @@ createChatItems ::
createChatItems user itemTs_ dirsCIContents = do
createdAt <- liftIO getCurrentTime
let itemTs = fromMaybe createdAt itemTs_
cxt <- chatStoreCxt'
cxt <- asks storeCxt
void . withStoreBatch' $ \db -> map (updateChat db cxt createdAt) dirsCIContents
withStoreBatch' $ \db -> concatMap (createACIs db itemTs createdAt) dirsCIContents
where
@@ -3390,13 +3379,9 @@ waitChatStartedAndActivated = do
unless (isJust started && activated) retry
chatStoreCxt :: CM StoreCxt
chatStoreCxt = lift chatStoreCxt'
chatStoreCxt = asks storeCxt
{-# INLINE chatStoreCxt #-}
chatStoreCxt' :: CM' StoreCxt
chatStoreCxt' = mkStoreCxt <$> asks config
{-# INLINE chatStoreCxt' #-}
chatVersionRange :: CM VersionRangeChat
chatVersionRange = lift chatVersionRange'
{-# INLINE chatVersionRange #-}
+144 -132
View File
@@ -115,9 +115,9 @@ smallGroupsRcptsMemLimit = 20
-- Verifies member signatures over CBGroup <> (publicGroupId, memberId) or (memberId, pubKey) <> signedBody under the given key.
-- signatures is NonEmpty so the verification can't be vacuously true.
verifyGroupSig :: C.PublicKeyEd25519 -> Maybe GroupKeys -> MemberId -> NonEmpty MsgSignature -> ByteString -> Bool
verifyGroupSig key gks memberId signatures signedBody =
let prefix = encodeChatBinding CBGroup $ groupBindingData gks memberId key
verifyGroupSig :: C.PublicKeyEd25519 -> GroupInfo -> MemberId -> NonEmpty MsgSignature -> ByteString -> Bool
verifyGroupSig key gInfo memberId signatures signedBody =
let prefix = encodeChatBinding CBGroup $ groupBindingData gInfo memberId key
in all (\case (MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 key) sig (prefix <> signedBody)) signatures
processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
@@ -138,13 +138,18 @@ processAgentMessage corrId connId msg = do
-- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert,
-- as in this case no need to ACK message - we can't process messages for this connection anyway.
critical connId (withStore $ getUserEntity cxt) >>= \case
Just (user, entity) -> processAgentMessageConn cxt user entity corrId connId msg `catchAllErrors` eToView
Just (user, entity, gks_) -> processAgentMessageConn cxt user entity gks_ corrId connId msg `catchAllErrors` eToView
_ -> throwChatError $ CENoConnectionUser (AgentConnId connId)
where
getUserEntity :: StoreCxt -> DB.Connection -> ExceptT StoreError IO (Maybe (User, ConnectionEntity))
getUserEntity :: StoreCxt -> DB.Connection -> ExceptT StoreError IO (Maybe (User, ConnectionEntity, Maybe GroupKeys))
getUserEntity cxt db =
liftIO (getUserByAConnId db $ AgentConnId connId)
>>= mapM (\user -> (user,) <$> (getConnectionEntity db cxt user (AgentConnId connId) >>= liftIO . updateConnStatus db))
>>= mapM (\user -> do
(entity, groupKeysData_) <- getConnectionEntityKeys db cxt user (AgentConnId connId)
gks_ <- case entity of
RcvGroupMsgConnection _ gInfo _ -> mapM (mkGroupKeys db cxt gInfo) groupKeysData_
_ -> pure Nothing
(user,,gks_) <$> liftIO (updateConnStatus db entity))
updateConnStatus :: DB.Connection -> ConnectionEntity -> IO ConnectionEntity
updateConnStatus db acEntity = case agentMsgConnStatus (entityConnection acEntity) msg of
@@ -431,8 +436,8 @@ processAgentMsgRcvFile _corrId aFileId msg = do
type ShouldDeleteGroupConns = Bool
processAgentMessageConn :: StoreCxt -> User -> ConnectionEntity -> ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMessage =
processAgentMessageConn :: StoreCxt -> User -> ConnectionEntity -> Maybe GroupKeys -> ACorrId -> ConnId -> AEvent 'AEConn -> CM ()
processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId agentMessage =
case agentMessage of
END -> case entity of
RcvDirectMsgConnection _ (Just ct) -> toView $ CEvtContactAnotherClient user ct
@@ -441,8 +446,9 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
_ -> case entity of
RcvDirectMsgConnection conn contact_ ->
processDirectMessage agentMessage entity conn contact_
RcvGroupMsgConnection conn gInfo m ->
processGroupMessage agentMessage entity conn gInfo m
RcvGroupMsgConnection conn gInfo m -> case gks_ of
Just gks -> processGroupMessage agentMessage entity conn (GIK gInfo gks) m
Nothing -> throwChatError $ CEInternalError "group connection entity without group keys"
UserContactConnection conn uc ->
processContactConnMessage agentMessage entity conn uc
where
@@ -497,10 +503,10 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId)
profileToSend <-
presentUserBadge user incognitoProfile $ case gInfo_ of
Just gInfo -> userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
Just (GIK gInfo _) -> userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
Nothing -> userProfileDirect user (fromLocalProfile <$> incognitoProfile) Nothing True
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn'' confId gInfo_ $ XInfo profileToSend (groupMemberKey =<< gInfo_)
allowAgentConnectionAsync user conn'' confId gInfo_ $ XInfo profileToSend ((\(GIK _ gks) -> groupMemberKey gks) <$> gInfo_)
INFO pqSupport connInfo -> do
processINFOpqSupport conn pqSupport
void $ saveConnInfo conn connInfo
@@ -620,6 +626,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
void $ withStore' $ \db -> resetMemberContactFields db ct'
XGrpLinkInv glInv -> do
-- XGrpLinkInv here means we are connecting via business contact card, so we replace contact with group
when (isPublicGroupInv glInv) $ throwChatError $ CEInvalidChatMessage conn'' Nothing (safeDecodeUtf8 connInfo) "x.grp.link.inv: publicGroup not allowed in p2p groups"
memberKeys <- atomically . C.generateKeyPair =<< asks random
(gInfo, host) <- withStore $ \db -> do
liftIO $ deleteContactCardKeepConn db connId ct
@@ -628,7 +635,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId)
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile)
allowAgentConnectionAsync user conn'' confId (Just gInfo) $ XInfo profileToSend (groupMemberKey gInfo)
let gks = GKGroup {memberPrivKey = snd memberKeys}
allowAgentConnectionAsync user conn'' confId (Just $ GIK gInfo gks) $ XInfo profileToSend (Just $ groupMemberKey gks)
toView $ CEvtBusinessLinkConnecting user gInfo host ct
_ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info"
INFO pqSupport connInfo -> do
@@ -754,8 +762,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
processGroupMessage agentMsg connEntity conn@Connection {connId, customUserProfileId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of
processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfoKeys -> GroupMember -> CM ()
processGroupMessage agentMsg connEntity conn@Connection {connId, customUserProfileId, connectionCode} g@(GIK gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} gks) m = case agentMsg of
INV (ACR _ cReq) ->
withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} ->
case cReq of
@@ -780,7 +788,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
forM_ mKey $ \(MemberKey k) -> withStore' $ \db -> setMemberPubKey db (groupMemberId' m) k
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn' confId (Just gInfo) XOk
allowAgentConnectionAsync user conn' confId (Just g) XOk
| otherwise -> messageError "x.grp.acpt: memberId is different from expected"
XGrpRelayAcpt relayLink relayCap
| memberRole' membership == GROwner && isRelay m -> do
@@ -800,7 +808,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
liftIO $ updateGroupMemberStatus db userId m GSMemLeft
pure (relay', m {memberStatus = GSMemLeft})
-- complete the contact handshake so the relay receives INFO and cleans up its transient bookkeeping
allowAgentConnectionAsync user conn' confId (Just gInfo) XOk
allowAgentConnectionAsync user conn' confId (Just g) XOk
toView $ CEvtGroupRelayUpdated user gInfo m' relay'
toViewTE $ TERelayRejected user gInfo reason
| otherwise -> messageError "x.grp.relay.reject: only owner should receive relay rejection"
@@ -812,12 +820,11 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
pgId = fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId),
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
gInfo' <- createUserMemberKey gInfo''
(gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db cxt user gInfo m glInv
-- [incognito] send saved profile
incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId)
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo' (fromLocalProfile <$> incognitoProfile)
allowAgentConnectionAsync user conn' confId (Just gInfo') $ XInfo profileToSend (groupMemberKey gInfo')
allowAgentConnectionAsync user conn' confId (Just $ GIK gInfo' gks) $ XInfo profileToSend (Just $ groupMemberKey gks)
toView $ CEvtGroupLinkConnecting user gInfo' m'
| otherwise -> messageError "x.grp.link.inv: publicGroupId mismatch"
XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do
@@ -833,7 +840,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
membershipProfile <- presentUserBadge user (incognitoMembershipProfile gInfo) $ redactedMemberProfile gInfo membership $ fromLocalProfile $ memberProfile membership
-- TODO update member profile
-- [async agent commands] no continuation needed, but command should be asynchronous for stability
allowAgentConnectionAsync user conn' confId (Just gInfo) $ XGrpMemInfo membershipMemId membershipProfile
allowAgentConnectionAsync user conn' confId (Just g) $ XGrpMemInfo membershipMemId membershipProfile
| 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
@@ -914,12 +921,12 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
Nothing -> do
withStore' $ \db -> setGroupRosterVersion db gInfo (VersionRoster 0)
pure gInfo {rosterVersion = Just (VersionRoster 0)}
sendGroupRosterToRelay user gInfo' m
sendGroupRosterToRelay user (GIK gInfo' gks) m
else do
-- a relay below groupRosterVersion can't ack a roster; publish it on connect as before
-- the handshake (getPublishableGroupRelays and the LINK handler include/activate it by version)
gLink <- withStore $ \db -> getGroupLink db user gInfo
setGroupLinkDataAsync user gInfo gLink
setGroupLinkDataAsync user g gLink
| otherwise -> do
(gInfo', mStatus) <-
if not (memberPending m)
@@ -940,26 +947,25 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
when (isJust viaUserContactLink && isNothing (memberContactId m')) $ sendXGrpLinkMem gInfo'' m'
if useRelays' gInfo''
then do
introduceInChannel cxt user gInfo'' m'
introduceInChannel cxt user (GIK gInfo'' gks) m'
case mStatus of
GSMemPendingApproval -> pure ()
GSMemPendingReview -> pure ()
_ -> when (groupFeatureAllowed SGFHistory gInfo'') $ sendHistory user gInfo'' m'
else case mStatus of
GSMemPendingApproval -> pure ()
GSMemPendingReview -> introduceToModerators cxt user gInfo'' m'
GSMemPendingReview -> introduceToModerators cxt user (GIK gInfo'' gks) m'
_ -> do
introduceToAll cxt user gInfo'' m'
introduceToAll cxt user (GIK gInfo'' gks) m'
let memberIsCustomer = case businessChat gInfo'' of
Just BusinessChatInfo {chatType = BCCustomer, customerId} -> memberId' m' == customerId
_ -> False
when (groupFeatureAllowed SGFHistory gInfo'' && not memberIsCustomer) $ sendHistory user gInfo'' m'
where
sendXGrpLinkMem gInfo''' m' = do
gInfo'' <- createUserMemberKey gInfo'''
sendXGrpLinkMem gInfo'' m' = do
let incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo''
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo'' (fromIncognitoProfile <$> incognitoProfile)
sendGroupMemberMessages user gInfo'' conn [XGrpLinkMem profileToSend (groupMemberKey gInfo'')]
sendGroupMemberMessages user (GIK gInfo'' gks) conn [XGrpLinkMem profileToSend (Just $ groupMemberKey gks)]
_ -> do
unless (memberPending m) $ withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected
notifyMemberConnected gInfo m Nothing
@@ -1023,7 +1029,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
case fwd_ of
Just fwd | SJson <- enc -> do
logInfo $ "group fwd=" <> tshow tag <> " " <> eInfo
xGrpMsgForward gInfo' scopeInfo m' fwd parsedMsg brokerTs
xGrpMsgForward (GIK gInfo' gks) scopeInfo m' fwd parsedMsg brokerTs
`catchAllErrors` \e -> eToView e
pure newDeliveryTasks
-- direct JSON and binary messages; binary events don't produce delivery tasks
@@ -1074,36 +1080,36 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
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 mKey -> Nothing <$ xGrpLinkMem gInfo' m'' conn' p mKey msg
XGrpLinkAcpt acceptance role memberId -> Nothing <$ xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs
XGrpLinkAcpt acceptance role memberId -> Nothing <$ xGrpLinkAcpt (GIK gInfo' gks) m'' acceptance role memberId msg brokerTs
XGrpRelayNew rl -> fmap ctx <$> xGrpRelayNew gInfo' m'' rl
XGrpRelayCap relayCap
| memberRole' membership == GROwner && isRelay m'' ->
Nothing <$ withStore' (\db -> updateRelayCapabilities db m'' relayCap)
| otherwise -> Nothing <$ messageWarning "x.grp.relay.cap: only owner should receive relay capabilities"
XGrpMemNew memInfo msgScope -> fmap ctx <$> xGrpMemNew gInfo' m'' memInfo msgScope msg brokerTs
XGrpMemNew memInfo msgScope -> fmap ctx <$> xGrpMemNew (GIK gInfo' gks) m'' memInfo msgScope msg brokerTs
XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_
XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv
XGrpMemFwd memInfo introInv -> Nothing <$ xGrpMemFwd gInfo' m'' memInfo introInv
XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole gInfo' Nothing m'' memId memRole memberKey rosterVer msg brokerTs
XGrpMemRole memId memRole memberKey rosterVer -> fmap ctx <$> xGrpMemRole (GIK gInfo' gks) Nothing m'' memId memRole memberKey rosterVer msg brokerTs
XGrpMemRestrict memId memRestrictions -> fmap ctx <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs
XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId
XGrpMemDel memId withMessages rosterVer -> case encoding @e of
SJson -> fmap ctx <$> xGrpMemDel gInfo' Nothing m'' memId withMessages rosterVer verifiedMsg msg brokerTs False
SJson -> fmap ctx <$> xGrpMemDel (GIK gInfo' gks) Nothing m'' memId withMessages rosterVer verifiedMsg msg brokerTs False
SBinary -> pure Nothing
XGrpLeave -> fmap ctx <$> xGrpLeave gInfo' m'' msg brokerTs
XGrpLeave -> fmap ctx <$> xGrpLeave (GIK gInfo' gks) m'' msg brokerTs
XGrpDel -> Just (DeliveryTaskContext (DJSGroup {jobSpec = DJRelayRemoved}) False) <$ xGrpDel gInfo' m'' msg brokerTs
XGrpInfo p' -> fmap ctx <$> xGrpInfo gInfo' m'' p' msg brokerTs
XGrpInfo p' -> fmap ctx <$> xGrpInfo (GIK gInfo' gks) m'' p' msg brokerTs
XGrpPrefs ps' -> fmap ctx <$> xGrpPrefs gInfo' m'' ps' msg
XGrpRoster gr -> fmap ctx <$> xGrpRoster gInfo' m'' m'' gr verifiedMsg sharedMsgId_ brokerTs
XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck gInfo' m'' ackVer ackErr
XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest gInfo' m'' reqVer
XGrpRosterAck ackVer ackErr -> Nothing <$ xGrpRosterAck (GIK gInfo' gks) m'' ackVer ackErr
XGrpRosterRequest reqVer -> Nothing <$ xGrpRosterRequest (GIK gInfo' gks) m'' reqVer
-- TODO [knocking] why don't we forward these messages?
XGrpDirectInv connReq mContent_ msgScope -> memberCanSend (Just m'') msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs
XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward gInfo' Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs
XGrpMsgForward fwd msg' -> Nothing <$ xGrpMsgForward (GIK gInfo' gks) Nothing m'' fwd (ParsedMsg Nothing Nothing msg') brokerTs
XInfoProbe probe -> Nothing <$ xInfoProbe (COMGroupMember m'') probe
XInfoProbeCheck probeHash -> Nothing <$ xInfoProbeCheck (COMGroupMember m'') probeHash
XInfoProbeOk probe -> Nothing <$ xInfoProbeOk (COMGroupMember m'') probe
BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' m'' sharedMsgId chunk msgMeta
BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup (GIK gInfo' gks) m'' sharedMsgId chunk msgMeta
_ -> Nothing <$ messageError ("unsupported message: " <> tshow event)
forM deliveryTaskContext_ $ \taskContext -> do
let contentChanged :: CM ()
@@ -1170,7 +1176,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
mapM_ toView fileEvent_
unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis
when continued $ do
when (isUserGrpFwdRelay gInfo) $ serveRoster user gInfo m -- roster ahead of the resumed backlog
when (isUserGrpFwdRelay gInfo) $ serveRoster user g m -- roster ahead of the resumed backlog
sendPendingGroupMessages user gInfo m conn
SWITCH qd phase cStats -> do
toView $ CEvtGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats)
@@ -1241,7 +1247,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
withStore' $ \db -> updateConnLinkData db user conn cReq cReqHash groupLinkId chatV pqSup
let incognitoProfile = fromLocalProfile <$> incognitoMembershipProfile gInfo
profileToSend <- presentUserBadge user incognitoProfile $ userProfileInGroup user gInfo incognitoProfile
dm <- encodeXMemberConnInfo gInfo relayMemberId profileToSend
dm <- encodeXMemberConnInfo g relayMemberId profileToSend
subMode <- chatReadVar subscriptionMode
(cmdId, connId') <- prepareAgentJoin user (Just conn) True cReq
joinAgentConnectionAsync cmdId True connId' True cReq dm subMode
@@ -1257,7 +1263,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
(m', relay) <- setRelayLinkAccepted db cxt user m (MemberKey relayKey) relayProfile
pure (confId, m', relay)
allowAgentConnectionAsync user conn confId (Just gInfo) XOk
allowAgentConnectionAsync user conn confId (Just g) XOk
toView $ CEvtGroupRelayUpdated user gInfo m' relay
else
-- TODO [relays] owner: TBC failed RelayStatus?
@@ -1266,7 +1272,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
QCONT -> do
continued <- continueSending connEntity conn
when continued $ do
when (isUserGrpFwdRelay gInfo) $ serveRoster user gInfo m -- roster ahead of the resumed backlog
when (isUserGrpFwdRelay gInfo) $ serveRoster user g m -- roster ahead of the resumed backlog
sendPendingGroupMessages user gInfo m conn
MWARN msgId err -> do
withStore' $ \db -> updateGroupItemsErrorStatus db msgId (groupMemberId' m) (GSSWarning $ agentSndError err)
@@ -1311,9 +1317,9 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
_ -> pure Nothing
sendGroupAutoReply mc = \case
Just UserContactRequest {welcomeSharedMsgId = Just smId} ->
void $ sendGroupMessage' user gInfo [m] $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing
void $ sendGroupMessage' user g [m] $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing Nothing
_ -> do
msg <- sendGroupMessage' user gInfo [m] $ XMsgNew $ mcSimple mc
msg <- sendGroupMessage' user g [m] $ XMsgNew $ mcSimple mc
ci <- saveSndChatItem user (CDGroupSnd gInfo Nothing) msg (CISndMsgContent mc)
withStore' $ \db -> createGroupSndStatus db (chatItemId' ci) (groupMemberId' m) GSSNew
toView $ CEvtNewChatItems user [AChatItem SCTGroup SMDSnd (GroupChat gInfo Nothing) ci]
@@ -1340,7 +1346,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
r n'' = Just (ci, CIRcvDecryptionError mde n'')
mdeUpdatedCI _ _ = Nothing
receiveFileChunk :: Maybe GroupInfo -> RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> CM ()
receiveFileChunk :: Maybe GroupInfoKeys -> RcvFileTransfer -> Maybe Connection -> MsgMeta -> FileChunk -> CM ()
receiveFileChunk gInfo_ ft@RcvFileTransfer {fileId, fileType, chunkSize} conn_ MsgMeta {recipient = (msgId, _), integrity} = \case
FileChunkCancel -> case fileType of
-- cancel only this source's transfer; other relays' in-flight transfers are independent
@@ -1408,13 +1414,13 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
CFSetShortLink ->
case (ucGroupId_, auData) of
(Just groupId, UserContactLinkData UserContactData {relays = relayLinks}) -> do
(gInfo, gLink, relays, relaysChanged, newlyActiveLinks) <- withStore $ \db -> do
gInfo <- getGroupInfo db cxt user groupId
(g@(GIK gInfo _), gLink, relays, relaysChanged, newlyActiveLinks) <- withStore $ \db -> do
g@(GIK gInfo _) <- getGroupInfoKeys db cxt user groupId
gLink <- getGroupLink db user gInfo
relays <- liftIO $ getGroupRelays db gInfo
(relays', changed, newlyActiveLinks) <- liftIO $ foldrM (updateRelay db) ([], False, []) relays
liftIO $ setGroupInProgressDone db gInfo
pure (gInfo, gLink, relays', changed, newlyActiveLinks)
pure (g, gLink, relays', changed, newlyActiveLinks)
toView $ CEvtGroupLinkDataUpdated user gInfo gLink relays relaysChanged
let GroupSummary {publicMemberCount} = groupSummary gInfo
-- Owner is counted in publicMemberCount; > 1 means at least one subscriber.
@@ -1430,7 +1436,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
allRelayMembers
events = XGrpRelayNew <$> newlyActive
unless (null recipients) $
void $ sendGroupMessages user gInfo Nothing False recipients False events
void $ sendGroupMessages user g Nothing False recipients False events
where
updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact]) -> IO ([GroupRelay], Bool, [ShortLinkContact])
updateRelay db relay@GroupRelay {relayLink, relayStatus} (acc, changed, newlyActiveLinks) =
@@ -1479,7 +1485,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
REContact ct ->
-- TODO [short links] update request msg
toView $ CEvtContactRequestAlreadyAccepted user ct
REBusinessChat gInfo _clientMember ->
REBusinessChat (GIK gInfo _) _clientMember ->
-- TODO [short links] update request msg
toView $ CEvtBusinessRequestAlreadyAccepted user gInfo
RSCurrentRequest prevUcr_ ucr@UserContactRequest {welcomeSharedMsgId} re_ -> case re_ of
@@ -1513,8 +1519,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
else pure Nothing
ct' <- acceptContactRequestAsync user uclId ct ucr incognitoProfile
toView $ CEvtAcceptingContactRequest user ct'
Just (REBusinessChat gInfo clientMember) -> do
(_gInfo', _clientMember') <- acceptBusinessJoinRequestAsync user uclId gInfo clientMember ucr
Just (REBusinessChat g@(GIK gInfo _) clientMember) -> do
(_gInfo', _clientMember') <- acceptBusinessJoinRequestAsync user uclId g clientMember ucr
let cd = CDGroupRcv gInfo Nothing clientMember
void $ case prevUcr_ of
Just UserContactRequest {requestSharedMsgId = prevSharedMsgId_} ->
@@ -1606,7 +1612,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- ##### Group link join requests (don't create contact requests) #####
Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do
-- TODO [short links] deduplicate request by xContactId?
gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId
g@(GIK gInfo _) <- withStore $ \db -> getGroupInfoKeys db cxt user groupId
if
| useRelays' gInfo ->
messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored direct join request from " <> displayName <> " (group uses relays)"
@@ -1617,7 +1623,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case
Right (acceptance, useRole) -> do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ Nothing welcomeMsgId_ acceptance useRole profileMode memberKey_ Nothing
mem <- acceptGroupJoinRequestAsync user uclId g 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'
@@ -1661,7 +1667,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
(_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId
case gLinkInfo_ of
Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do
gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId
g@(GIK gInfo _) <- withStore $ \db -> getGroupInfoKeys db cxt user groupId
existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db cxt user gInfo joiningMemberId)
case existing_ of
Just rosterMem
@@ -1669,21 +1675,21 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- possession of that exact key, otherwise this is an attempt to impersonate it
| isRosterRole (memberRole' rosterMem) ->
if verifyKey gInfo rosterMem
then acceptJoin gInfo (Just rosterMem) (memberRole' rosterMem)
then acceptJoin g (Just rosterMem) (memberRole' rosterMem)
else messageError "memberJoinRequestViaRelay: rejected join claiming privileged memberId (key mismatch or invalid signature)"
_ -> acceptJoin gInfo Nothing gLinkMemRole
_ -> acceptJoin g Nothing gLinkMemRole
Nothing ->
messageError "memberJoinRequestViaRelay: no group link info for relay link"
where
-- replay defense: the viaRelay == own memberId check (viaRelay is in the signed body); without it a sibling relay could replay a privileged member's signed join
verifyKey gInfo rosterMem = case (signedMsg_, groupKeys gInfo) of
(Just SignedMsg {chatBinding = CBGroup, signatures, signedBody}, Just gks) ->
verifyKey gInfo rosterMem = case signedMsg_ of
Just SignedMsg {chatBinding = CBGroup, signatures, signedBody} ->
memberPubKey rosterMem == Just joiningKey
&& verifyGroupSig joiningKey (Just gks) joiningMemberId signatures signedBody
&& verifyGroupSig joiningKey gInfo joiningMemberId signatures signedBody
&& viaRelay == Just (memberId' (membership gInfo))
_ -> False
acceptJoin gInfo existingMem_ acceptRole = do
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted acceptRole Nothing (Just joiningMemberKey) existingMem_
acceptJoin g@(GIK gInfo _) existingMem_ acceptRole = do
mem <- acceptGroupJoinRequestAsync user uclId g invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted acceptRole Nothing (Just joiningMemberKey) existingMem_
(gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem'
@@ -2592,8 +2598,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- A group BFileChunk is a normal inline file chunk or a roster blob chunk, both located by
-- (group_id, shared_msg_id). A chunk matching no in-flight transfer (an orphaned re-served roster
-- chunk, or a missing normal file) is ignored; the outer withAckMessage acks it.
bFileChunkGroup :: GroupInfo -> GroupMember -> SharedMsgId -> FileChunk -> MsgMeta -> CM ()
bFileChunkGroup gInfo@GroupInfo {groupId} fromMember sharedMsgId chunk meta = do
bFileChunkGroup :: GroupInfoKeys -> GroupMember -> SharedMsgId -> FileChunk -> MsgMeta -> CM ()
bFileChunkGroup gInfo@(GIK GroupInfo {groupId} _) fromMember sharedMsgId chunk meta = do
fileId_ <- withStore' $ \db -> getGroupRcvFileId db userId groupId (groupMemberId' fromMember) sharedMsgId
forM_ fileId_ $ \fileId -> do
ft <- withStore $ \db -> getRcvFileTransfer db user fileId
@@ -2613,7 +2619,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- A roster re-serve re-sends the blob from chunk 1; discard any partial first, else chunk 1 over a
-- partial is out-of-order (RcvChunkError) and appending after the stale prefix corrupts the blob.
receiveRosterChunk :: GroupInfo -> RcvFileTransfer -> MsgMeta -> FileChunk -> CM ()
receiveRosterChunk :: GroupInfoKeys -> RcvFileTransfer -> MsgMeta -> FileChunk -> CM ()
receiveRosterChunk gInfo ft meta chunk = do
case chunk of
FileChunk {chunkNo} | chunkNo == 1 -> do
@@ -2678,14 +2684,14 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId
-- [incognito] if direct connection with host is incognito, create membership using the same incognito profile
memberKeys <- atomically . C.generateKeyPair =<< asks random
(gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId memberKeys
(GIK gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership} gks, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId memberKeys
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let GroupMember {groupMemberId, memberId = membershipMemId} = membership
-- hostContact is only reported for group links, where the client replaces
-- the transient host connection view with the group and removes its chat
joinGroupAsync hostContact_ sameLink = do
subMode <- chatReadVar subscriptionMode
dm <- encodeConnInfo $ XGrpAcpt membershipMemId (groupMemberKey gInfo)
dm <- encodeConnInfo $ XGrpAcpt membershipMemId (Just $ groupMemberKey gks)
connIds@(cmdId, acId) <- prepareAgentJoin user Nothing True connRequest
withStore' $ \db -> do
when sameLink $ setViaGroupLinkUri db groupId connId
@@ -2815,11 +2821,11 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
| otherwise -> messageError "member key not signed by that key, ignored"
where
signed = case signedMsg_ of
Just SignedMsg {chatBinding = CBGroup, signatures, signedBody} -> verifyGroupSig k (groupKeys gInfo) memberId signatures signedBody
Just SignedMsg {chatBinding = CBGroup, signatures, signedBody} -> verifyGroupSig k gInfo memberId signatures signedBody
_ -> False
xGrpLinkAcpt :: GroupInfo -> GroupMember -> GroupAcceptance -> GroupMemberRole -> MemberId -> RcvMessage -> UTCTime -> CM ()
xGrpLinkAcpt gInfo@GroupInfo {membership} m acceptance role memberId msg brokerTs
xGrpLinkAcpt :: GroupInfoKeys -> GroupMember -> GroupAcceptance -> GroupMemberRole -> MemberId -> RcvMessage -> UTCTime -> CM ()
xGrpLinkAcpt g@(GIK gInfo@GroupInfo {membership} _) m acceptance role memberId msg brokerTs
| memberRole' m < GRModerator || memberRole' m < role =
messageError "x.grp.link.acpt with insufficient member permissions"
| sameMemberId memberId membership = processUserAccepted
@@ -2868,7 +2874,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
GAPendingApproval ->
messageWarning "x.grp.link.acpt: unexpected group acceptance - pending approval"
introduceToRemainingMembers acceptedMember = do
introduceToRemaining cxt user gInfo acceptedMember
introduceToRemaining cxt user g acceptedMember
when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo acceptedMember
maybeCreateGroupDescrLocal :: GroupInfo -> GroupMember -> CM ()
@@ -3144,7 +3150,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
toView $ CEvtContactAndMemberAssociated user c2 g m1 c2'
pure c2'
saveConnInfo :: Connection -> ConnInfo -> CM (Connection, Maybe GroupInfo)
saveConnInfo :: Connection -> ConnInfo -> CM (Connection, Maybe GroupInfoKeys)
saveConnInfo activeConn connInfo = do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo
conn' <- updatePeerChatVRange activeConn chatVRange
@@ -3154,21 +3160,25 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
toView $ CEvtContactConnecting user ct
pure (conn', Nothing)
XGrpLinkInv glInv -> do
when (isPublicGroupInv glInv) $ throwChatError $ CEInvalidChatMessage conn' Nothing (safeDecodeUtf8 connInfo) "x.grp.link.inv: publicGroup not allowed in p2p groups"
memberKeys <- atomically . C.generateKeyPair =<< asks random
(gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db cxt user conn' memberKeys glInv
toView $ CEvtGroupLinkConnecting user gInfo host
pure (conn', Just gInfo)
pure (conn', Just $ GIK gInfo GKGroup {memberPrivKey = snd memberKeys})
XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do
memberKeys <- atomically . C.generateKeyPair =<< asks random
(gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db cxt user conn' memberKeys glRjct
toView $ CEvtGroupLinkConnecting user gInfo host
toViewTE $ TEGroupLinkRejected user gInfo rejectionReason
pure (conn', Just gInfo)
pure (conn', Just $ GIK gInfo GKGroup {memberPrivKey = snd memberKeys})
-- TODO show/log error, other events in SMP confirmation
_ -> pure (conn', Nothing)
xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ _ assertedKey_) msgScope_ msg brokerTs = do
isPublicGroupInv :: GroupLinkInvitation -> Bool
isPublicGroupInv GroupLinkInvitation {groupProfile = GroupProfile {publicGroup}} = isJust publicGroup
xGrpMemNew :: GroupInfoKeys -> GroupMember -> MemberInfo -> Maybe MsgScope -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpMemNew (GIK gInfo gks) m memInfo@(MemberInfo memId memRole _ _ assertedKey_) msgScope_ msg brokerTs = do
unless (useRelays' gInfo) $ checkHostRole m memRole
if sameMemberId memId (membership gInfo)
then pure Nothing
@@ -3188,7 +3198,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
messageWarning $ "x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" <> safeDecodeUtf8 (strEncode memId)
updatedMember <- withStore $ \db -> updateRosterMemberAnnounced db cxt user m unknownMember memInfo initialStatus
-- roster members can't be pending, so no members-require-attention update
gInfo' <- updatePublicGroupData user gInfo
gInfo' <- updatePublicGroupData user gInfo gks
toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember
memberAnnouncedToView updatedMember gInfo'
pure $ deliveryJobScope updatedMember
@@ -3203,7 +3213,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
then liftIO $ increaseGroupMembersRequireAttention db user gInfo
else pure gInfo
pure (updatedMember, gInfo')
gInfo'' <- updatePublicGroupData user gInfo'
gInfo'' <- updatePublicGroupData user gInfo' gks
toView $ CEvtUnknownMemberAnnounced user gInfo'' m unknownMember updatedMember
memberAnnouncedToView updatedMember gInfo''
pure $ deliveryJobScope updatedMember
@@ -3221,7 +3231,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
then liftIO $ increaseGroupMembersRequireAttention db user gInfo
else pure gInfo
pure (newMember, gInfo')
gInfo'' <- updatePublicGroupData user gInfo'
gInfo'' <- updatePublicGroupData user gInfo' gks
memberAnnouncedToView newMember gInfo''
pure $ deliveryJobScope newMember
where
@@ -3341,8 +3351,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- batch), then advance it in the same transaction; a strictly lower version is a replay and is ignored.
-- Only an owner sender may advance it: a non-owner signed event is rejected by the action that follows,
-- but must not bump roster_version first, or every later owner roster at a lower version is dropped.
applyAtRosterVersion :: GroupInfo -> Maybe GroupMember -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope)
applyAtRosterVersion gInfo fwdRelay_ sender rosterVer_ action
applyAtRosterVersion :: GroupInfoKeys -> Maybe GroupMember -> GroupMember -> Maybe VersionRoster -> CM (Maybe DeliveryJobScope) -> CM (Maybe DeliveryJobScope)
applyAtRosterVersion g@(GIK gInfo _) fwdRelay_ sender rosterVer_ action
| not (useRelays' gInfo) = action
| otherwise = case rosterVer_ of
Nothing -> action
@@ -3380,19 +3390,19 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
| otherwise = case fwdRelay_ of
Just relay
| gap, relay `supportsVersion` groupRosterVersion ->
void $ sendGroupMessage' user gInfo [relay] (XGrpRosterRequest prevComplete)
void $ sendGroupMessage' user g [relay] (XGrpRosterRequest prevComplete)
_ -> pure ()
where
gap = v > nextCompleteVersion prevComplete
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
xGrpMemRole :: GroupInfoKeys -> Maybe GroupMember -> GroupMember -> MemberId -> GroupMemberRole -> Maybe MemberKey -> Maybe VersionRoster -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpMemRole g@(GIK 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_ $
applyAtRosterVersion g fwdRelay_ m rosterVer_ $
let gInfo' = gInfo {membership = membership {memberRole = memRole}}
in changeMemberRole gInfo' membership False (\db -> updateGroupMemberRole db user membership memRole) (RGEUserRole memRole) True
| otherwise = applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ do
| otherwise = applyAtRosterVersion g fwdRelay_ m rosterVer_ $ do
defaultRole <- unknownMemberRole gInfo
-- an owner-signed event with a key TOFU-creates an unknown member only for a roster role; else a plain lookup
let allowCreate = useRelays' gInfo && senderRole == GROwner && isRosterRole memRole && isJust memberKey_
@@ -3486,8 +3496,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- Blob arrived: verify the owner-attested digest over the plaintext and guard against
-- downgrade before applying; on a relay, ack the owner and re-serve to members.
rosterCompletion :: GroupInfo -> RcvFileTransfer -> CM ()
rosterCompletion gInfo RcvFileTransfer {fileId, fileStatus} =
rosterCompletion :: GroupInfoKeys -> RcvFileTransfer -> CM ()
rosterCompletion g@(GIK gInfo _) RcvFileTransfer {fileId, fileStatus} =
withStore' (\db -> getRosterTransfer db fileId) >>= \case
-- defensive: the file always has its transfer (created together, deleted together)
Nothing -> lift (closeFileHandle fileId rcvFiles) >> forM_ (rosterFilePath fileStatus) removeFsFile
@@ -3497,7 +3507,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
let isRelay' = isUserGrpFwdRelay gInfo
ackErr err = do
cleanupRosterTransferById transferId
when isRelay' $ forM_ owner_ $ \owner -> sendRosterAck gInfo owner pendingVer (Just err)
when isRelay' $ forM_ owner_ $ \owner -> sendRosterAck g owner pendingVer (Just err)
if FD.FileDigest (LC.sha512Hash (LB.fromStrict blob)) /= pendingDigest
then ackErr "relay could not verify the roster blob"
else case parseAll rosterBlobP blob of
@@ -3522,7 +3532,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
emitRosterResults gInfo author rosterBrokerTs results
-- ack while setting up (own status accepted/acknowledged); a serving (active) relay must not ack broadcasts.
when (isRelay' && (relayOwnStatus gInfo == Just RSAccepted || relayOwnStatus gInfo == Just RSAcknowledgedRoster)) $ do
sendRosterAck gInfo author pendingVer Nothing
sendRosterAck g author pendingVer Nothing
withStore' $ \db -> void $ updateRelayOwnStatusFromTo db gInfo RSAccepted RSAcknowledgedRoster
where
rosterFilePath = \case
@@ -3588,11 +3598,11 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
else pure (gInfo, author)
toView CEvtMemberRole {user, groupInfo = gInfo', byMember = author', member, fromRole, toRole, msgSigned = Just MSSVerified}
sendRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM ()
sendRosterAck :: GroupInfoKeys -> GroupMember -> VersionRoster -> Maybe Text -> CM ()
sendRosterAck gInfo owner ackVer err = void $ sendGroupMessage' user gInfo [owner] (XGrpRosterAck ackVer err)
xGrpRosterAck :: GroupInfo -> GroupMember -> VersionRoster -> Maybe Text -> CM ()
xGrpRosterAck gInfo m ackVer err = do
xGrpRosterAck :: GroupInfoKeys -> GroupMember -> VersionRoster -> Maybe Text -> CM ()
xGrpRosterAck g@(GIK gInfo _) m ackVer err = do
relay_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupRelayByGMId db (groupMemberId' m))
case relay_ of
Just relay@GroupRelay {relayStatus = RSAccepted} -> case err of
@@ -3602,7 +3612,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
relay' <- liftIO $ updateRelayStatus db relay RSAcknowledgedRoster
gLink <- getGroupLink db user gInfo
pure (relay', gLink)
setGroupLinkDataAsync user gInfo gLink
setGroupLinkDataAsync user g gLink
toView $ CEvtGroupRelayUpdated user gInfo m relay'
| otherwise -> messageWarning "x.grp.roster.ack: stale version, awaiting ack for the current roster"
Just e -> do
@@ -3616,13 +3626,13 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- - the latter bounds reflected amplification (a member can't re-trigger a full serve). Gating on the stored
-- blob (not roster_version, the gate) means the relay serves only a blob the requester will accept.
-- serveRoster records the served version (on all serve paths) and is a no-op without a roster.
xGrpRosterRequest :: GroupInfo -> GroupMember -> Maybe VersionRoster -> CM ()
xGrpRosterRequest gInfo m reqVer_ =
xGrpRosterRequest :: GroupInfoKeys -> GroupMember -> Maybe VersionRoster -> CM ()
xGrpRosterRequest g@(GIK gInfo _) m reqVer_ =
when (isUserGrpFwdRelay gInfo) $ do
(stored_, served_) <- withStore' $ \db ->
(,) <$> getStoredRosterVersion db gInfo <*> getMemberRosterServedVersion db m
forM_ stored_ $ \stored ->
when (maybe True (stored >) reqVer_ && maybe True (stored >) served_) $ serveRoster user gInfo m
when (maybe True (stored >) reqVer_ && maybe True (stored >) served_) $ serveRoster user g m
checkHostRole :: GroupMember -> GroupMemberRole -> CM ()
checkHostRole GroupMember {memberRole, localDisplayName} memRole =
@@ -3668,11 +3678,11 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected
withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected
xGrpMemDel :: GroupInfo -> Maybe GroupMember -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope)
xGrpMemDel gInfo@GroupInfo {membership} fwdRelay_ m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do
xGrpMemDel :: GroupInfoKeys -> Maybe GroupMember -> GroupMember -> MemberId -> Bool -> Maybe VersionRoster -> VerifiedMsg 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe DeliveryJobScope)
xGrpMemDel g@(GIK gInfo@GroupInfo {membership} gks) fwdRelay_ m@GroupMember {memberRole = senderRole} memId withMessages rosterVer_ verifiedMsg msg@RcvMessage {msgSigned} brokerTs forwarded = do
let GroupMember {memberId = membershipMemId} = membership
if membershipMemId == memId
then applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $ checkRole membership $ do
then applyAtRosterVersion g fwdRelay_ m rosterVer_ $ checkRole membership $ do
deleteGroupLinkIfExists user gInfo
-- TODO [relays] possible improvement is to immediately delete rcv queues if isUserGrpFwdRelay
unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False
@@ -3684,7 +3694,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
deleteMemberItem msg gInfo RGEUserDeleted
toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned
pure $ Just DJSGroup {jobSpec = DJRelayRemoved}
else applyAtRosterVersion gInfo fwdRelay_ m rosterVer_ $
else applyAtRosterVersion g fwdRelay_ m rosterVer_ $
withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case
Left _ -> do
messageError "x.grp.mem.del with unknown member ID"
@@ -3710,7 +3720,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
fullyDeleteMemberRecord user gInfo deletedMember
-- Undeleted "member connected" chat item will prevent deletion of member record.
| otherwise -> deleteOrUpdateMemberRecord user gInfo deletedMember
gInfo'' <- updatePublicGroupData user gInfo'
gInfo'' <- updatePublicGroupData user gInfo' gks
let wasDeleted = memberStatus == GSMemRemoved || memberStatus == GSMemLeft
-- Clear forwardedByMember if it references the deleted member,
-- as the member record was already deleted above.
@@ -3752,12 +3762,12 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
| useRelays' gInfo = asks $ channelSubscriberRole . config
| otherwise = pure GRAuthor
xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpLeave gInfo m msg@RcvMessage {msgSigned} brokerTs = do
xGrpLeave :: GroupInfoKeys -> GroupMember -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpLeave (GIK gInfo gks) m msg@RcvMessage {msgSigned} brokerTs = do
deleteMemberConnection m
-- member record is not deleted to allow creation of "member left" chat item
gInfo' <- updateMemberRecordDeleted user gInfo m GSMemLeft
gInfo'' <- updatePublicGroupData user gInfo'
gInfo'' <- updatePublicGroupData user gInfo' gks
unless (muteEventInChannel gInfo'' m) $ do
(gInfo''', m', scopeInfo) <- mkGroupChatScope gInfo'' m
(ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo''' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEMemberLeft)
@@ -3777,8 +3787,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
groupMsgToView cInfo ci
toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' msgSigned
xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {publicGroup = pg}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {publicGroup = pg'} msg@RcvMessage {msgSigned} brokerTs
xGrpInfo :: GroupInfoKeys -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpInfo gik@(GIK g@GroupInfo {groupProfile = p@GroupProfile {publicGroup = pg}, businessChat} gks) m@GroupMember {memberRole} p'@GroupProfile {publicGroup = pg'} msg@RcvMessage {msgSigned} brokerTs
| memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" $> Nothing
| let pgId = fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId),
useRelays' g && (isNothing pg' || pgId pg' /= pgId pg) = messageError "x.grp.info: publicGroupId mismatch for channel" $> Nothing
@@ -3798,10 +3808,10 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
-- other owners receiving the update do not refresh the same link
ChatConfig {updateGroupLinksFromApp} <- asks config
unless (useRelays' g'' || updateGroupLinksFromApp) $
void $ forkIO $ void $ setGroupLinkData' NRMBackground user g''
void $ forkIO $ void $ setGroupLinkData' NRMBackground user (GIK g'' gks)
Just _ -> updateGroupPrefs_ msgSigned g m $ fromMaybe defaultBusinessGroupPrefs $ groupPreferences p'
-- relay advertises its web capability now that the owner's version is known (bumped by saveGroupRcvMsg)
when (isRelay (membership g)) $ sendRelayCapIfNeeded user g
when (isRelay (membership g)) $ sendRelayCapIfNeeded user gik
pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}}
xGrpPrefs :: GroupInfo -> GroupMember -> GroupPreferences -> RcvMessage -> CM (Maybe DeliveryJobScope)
@@ -3916,8 +3926,8 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
toViewTE $ TEContactVerificationReset user ct
createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing
xGrpMsgForward :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> GrpMsgForward -> ParsedMsg 'Json -> UTCTime -> CM ()
xGrpMsgForward gInfo scopeInfo m@GroupMember {localDisplayName} GrpMsgForward {fwdSender, fwdBrokerTs = msgTs} parsedMsg@(ParsedMsg _ _ chatMsg@ChatMessage {chatMsgEvent}) brokerTs = do
xGrpMsgForward :: GroupInfoKeys -> Maybe GroupChatScopeInfo -> GroupMember -> GrpMsgForward -> ParsedMsg 'Json -> UTCTime -> CM ()
xGrpMsgForward g@(GIK gInfo _) scopeInfo m@GroupMember {localDisplayName} GrpMsgForward {fwdSender, fwdBrokerTs = msgTs} parsedMsg@(ParsedMsg _ _ chatMsg@ChatMessage {chatMsgEvent}) brokerTs = do
unless (isMemberGrpFwdRelay gInfo m) $ throwChatError (CEGroupContactRole localDisplayName)
case fwdSender of
FwdMember memberId memberName -> do
@@ -3961,13 +3971,13 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
XFileCancel sharedMsgId -> void $ xFileCancelGroup gInfo author_ sharedMsgId
XInfo p mKey -> withAuthor XInfo_ $ \author -> void $ xInfoMember gInfo author p mKey rcvMsg msgTs
XGrpRelayNew rl -> withAuthor XGrpRelayNew_ $ \author -> void $ xGrpRelayNew gInfo author rl
XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs
XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole gInfo (Just m) author memId memRole memberKey rosterVer rcvMsg msgTs
XGrpMemNew memInfo msgScope -> withAuthor XGrpMemNew_ $ \author -> void $ xGrpMemNew g author memInfo msgScope rcvMsg msgTs
XGrpMemRole memId memRole memberKey rosterVer -> withAuthor XGrpMemRole_ $ \author -> void $ xGrpMemRole g (Just m) author memId memRole memberKey rosterVer rcvMsg msgTs
XGrpMemRestrict memId memRestrictions -> withAuthor XGrpMemRestrict_ $ \author -> void $ xGrpMemRestrict gInfo author memId memRestrictions rcvMsg msgTs
XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel gInfo (Just m) author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True
XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave gInfo author rcvMsg msgTs
XGrpMemDel memId withMessages rosterVer -> withAuthor XGrpMemDel_ $ \author -> void $ xGrpMemDel g (Just m) author memId withMessages rosterVer verifiedMsg rcvMsg msgTs True
XGrpLeave -> withAuthor XGrpLeave_ $ \author -> void $ xGrpLeave g author rcvMsg msgTs
XGrpDel -> withAuthor XGrpDel_ $ \author -> void $ xGrpDel gInfo author rcvMsg msgTs
XGrpInfo p' -> withAuthor XGrpInfo_ $ \author -> void $ xGrpInfo gInfo author p' rcvMsg msgTs
XGrpInfo p' -> withAuthor XGrpInfo_ $ \author -> void $ xGrpInfo g author p' rcvMsg msgTs
XGrpPrefs ps' -> withAuthor XGrpPrefs_ $ \author -> void $ xGrpPrefs gInfo author ps' rcvMsg
XGrpRoster gr -> withAuthor XGrpRoster_ $ \author -> void $ xGrpRoster gInfo m author gr verifiedMsg sharedMsgId_ msgTs
_ -> messageError $ "x.grp.msg.forward: unsupported forwarded event " <> T.pack (show $ toCMEventTag event)
@@ -3978,7 +3988,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
Nothing -> messageError $ "x.grp.msg.forward: event " <> tshow tag <> " requires author"
withVerifiedMsg :: GroupInfo -> Maybe GroupChatScopeInfo -> GroupMember -> ParsedMsg e -> UTCTime -> (VerifiedMsg e -> CM a) -> CM (Maybe a)
withVerifiedMsg gInfo@GroupInfo {membership, groupKeys} scopeInfo member@GroupMember {memberPubKey, memberId} (ParsedMsg _ signedMsg_ chatMsg@ChatMessage {chatMsgEvent}) ts action =
withVerifiedMsg gInfo@GroupInfo {membership} scopeInfo member@GroupMember {memberPubKey, memberId} (ParsedMsg _ signedMsg_ chatMsg@ChatMessage {chatMsgEvent}) ts action =
case verified of
Just verifiedMsg -> Just <$> action verifiedMsg
Nothing -> do
@@ -3988,7 +3998,7 @@ processAgentMessageConn cxt user@User {userId} entity corrId agentConnId agentMe
verified = case signedMsg_ of
Just sm@SignedMsg {chatBinding, signatures, signedBody} -> case memberPubKey of
Just pubKey -> case chatBinding of
CBGroup -> signed MSSVerified <$ guard (verifyGroupSig pubKey groupKeys memberId signatures signedBody)
CBGroup -> signed MSSVerified <$ guard (verifyGroupSig pubKey gInfo memberId signatures signedBody)
_ -> signed MSSSignedNoKey <$ guard signatureOptional
Nothing -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag)
where
@@ -4476,10 +4486,10 @@ runRelayRequestWorker a Worker {doWork} = do
eToView e
processRelayRequest :: GroupId -> RelayRequestData -> CM ()
processRelayRequest groupId rrd = do
(gInfo, groupLink_) <- withStore $ \db -> do
gInfo <- getGroupInfo db cxt user groupId
(g@(GIK gInfo _), groupLink_) <- withStore $ \db -> do
g@(GIK gInfo _) <- getGroupInfoKeys db cxt user groupId
groupLink_ <- liftIO $ runExceptT $ getGroupLink db user gInfo
pure (gInfo, groupLink_)
pure (g, groupLink_)
-- Check if relay link already exists (recovery case)
case groupLink_ of
Right GroupLink {connLinkContact = CCLink _ sLnk_} ->
@@ -4487,11 +4497,14 @@ runRelayRequestWorker a Worker {doWork} = do
Just sLnk -> acceptOwnerConnection rrd gInfo sLnk
Nothing -> throwChatError $ CEException "processRelayRequest: relay link doesn't have short link"
Left _ -> do
(gInfo', sLnk) <- getLinkDataCreateRelayLink rrd gInfo
(gInfo', sLnk) <- getLinkDataCreateRelayLink rrd g
acceptOwnerConnection rrd gInfo' sLnk
where
getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfo -> CM (GroupInfo, ShortLinkContact)
getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} gInfo = do
getLinkDataCreateRelayLink :: RelayRequestData -> GroupInfoKeys -> CM (GroupInfo, ShortLinkContact)
getLinkDataCreateRelayLink RelayRequestData {reqGroupLink} (GIK gInfo gks) = do
memberPrivKey' <- case gks of
GKRelayRequest {memberPrivKey} -> pure memberPrivKey
_ -> throwChatError $ CEException "getLinkDataCreateRelayLink: group is not a relay request"
(FixedLinkData {linkEntityId, rootKey}, cData@(ContactLinkData _ UserContactData {owners}), _) <- getShortLinkConnReq' NRMBackground user reqGroupLink
liftIO (decodeLinkUserData cData) >>= \case
Nothing -> throwChatError $ CEException "getLinkDataCreateRelayLink: no group link data"
@@ -4501,10 +4514,10 @@ runRelayRequestWorker a Worker {doWork} = do
| B64UrlByteString entityId == publicGroupId -> pure pg
_ -> throwChatError $ CEException "getLinkDataCreateRelayLink: linkEntityId does not match profile publicGroupId"
validateGroupProfile gp
((_, memberPrivKey), sLnk) <- createRelayLink gInfo
sLnk <- createRelayLink gInfo (C.publicKey memberPrivKey', memberPrivKey')
gInfo' <- withStore $ \db -> do
void $ updateGroupProfile db user gInfo gp
updateRelayGroupKeys db user gInfo pg rootKey memberPrivKey owners
updateRelayGroupKeys db user gInfo pg rootKey owners
getGroupInfo db cxt user groupId
pure (gInfo', sLnk)
where
@@ -4512,14 +4525,13 @@ runRelayRequestWorker a Worker {doWork} = do
validateGroupProfile _groupProfile = do
-- TODO [relays] relay: validate group profile, verify owner's signature
pure ()
createRelayLink :: GroupInfo -> CM (C.KeyPairEd25519, ShortLinkContact)
createRelayLink gi = do
createRelayLink :: GroupInfo -> C.KeyPairEd25519 -> CM ShortLinkContact
createRelayLink gi sigKeys = do
let GroupInfo {membership} = gi
GroupMember {memberId = MemberId relayMemId, memberProfile = p} = membership
gVar <- asks random
groupLinkId <- GroupLinkId <$> drgRandomBytes 16
subMode <- chatReadVar subscriptionMode
sigKeys <- atomically $ C.generateKeyPair gVar
let crClientData = encodeJSON $ CRDataGroup groupLinkId
-- prepare link with relayMemId as linkEntityId (no server request)
(ccLink, preparedParams) <- withAgent $ \a' -> prepareConnectionLink a' (aUserId user) sigKeys relayMemId True (Just crClientData) CR.IKPQOff False Nothing
@@ -4534,7 +4546,7 @@ runRelayRequestWorker a Worker {doWork} = do
-- TODO [relays] starting role should be communicated in protocol from owner to relays
subRole <- asks $ channelSubscriberRole . config
void $ withFastStore $ \db -> createGroupLink db gVar user gi connId ccLink' groupLinkId subRole subMode
pure (sigKeys, sLnk)
pure sLnk
acceptOwnerConnection :: RelayRequestData -> GroupInfo -> ShortLinkContact -> CM ()
acceptOwnerConnection RelayRequestData {relayInvId, reqChatVRange} gi relayLink = do
ownerMember <- withStore $ \db -> getHostMember db cxt user groupId
+14 -8
View File
@@ -12,6 +12,7 @@
module Simplex.Chat.Store.Connections
( getChatLockEntity,
getConnectionEntity,
getConnectionEntityKeys,
getConnectionEntityByConnReq,
getConnectionEntityViaShortLink,
getContactConnEntityByConnReqHash,
@@ -76,18 +77,23 @@ getChatLockEntity db agentConnId = do
-- - from receiving: getConnectionEntity, getContactConnEntityByConnReqHash
-- - from subscribing: getContactConnsToSub, getUCLConnsToSub, getMemberConnsToSub, getPendingConnsToSub
getConnectionEntity :: DB.Connection -> StoreCxt -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity
getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do
getConnectionEntity db cxt user agentConnId = fst <$> getConnectionEntityKeys db cxt user agentConnId
getConnectionEntityKeys :: DB.Connection -> StoreCxt -> User -> AgentConnId -> ExceptT StoreError IO (ConnectionEntity, Maybe GroupKeysRow)
getConnectionEntityKeys db cxt user@User {userId, userContactId} agentConnId = do
c@Connection {connType, entityId} <- getConnection_
case entityId of
Nothing ->
if connType == ConnContact
then pure $ RcvDirectMsgConnection c Nothing
then pure (RcvDirectMsgConnection c Nothing, Nothing)
else throwError $ SEInternalError $ "connection " <> show connType <> " without entity"
Just entId ->
case connType of
ConnMember -> uncurry (RcvGroupMsgConnection c) <$> getGroupAndMember_ entId c
ConnContact -> RcvDirectMsgConnection c . Just <$> getContactRec_ entId c
ConnUserContact -> UserContactConnection c <$> getUserContact_ entId
ConnMember -> do
((gInfo, keysData), m) <- getGroupAndMember_ entId c
pure (RcvGroupMsgConnection c gInfo m, Just keysData)
ConnContact -> (,Nothing) . RcvDirectMsgConnection c . Just <$> getContactRec_ entId c
ConnUserContact -> (,Nothing) . UserContactConnection c <$> getUserContact_ entId
where
getConnection_ :: ExceptT StoreError IO Connection
getConnection_ = ExceptT $ do
@@ -134,7 +140,7 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do
contactRequest = UserContactRequestRef <$> contactRequestId <*> (unBI <$> rejectionSupported_)
groupDirectInv = toGroupDirectInvitation groupDirectInvRow
in Contact {contactId, localDisplayName, profile, activeConn, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, preparedContact, contactRequestId, contactRequest, contactGroupMemberId, contactGrpInvSent, groupDirectInv, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO ((GroupInfo, GroupKeysRow), GroupMember)
getGroupAndMember_ groupMemberId c = do
currentTs <- liftIO getCurrentTime
gm <-
@@ -178,8 +184,8 @@ getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do
AND mu.member_status NOT IN (?,?,?)
|]
(groupMemberId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted)
liftIO $ bitraverse (addGroupChatTags db) pure gm
toGroupAndMember :: UTCTime -> Connection -> GroupInfoRow :. GroupMemberRow -> (GroupInfo, GroupMember)
liftIO $ bitraverse (\(g, keysData) -> (,keysData) <$> addGroupChatTags db g) pure gm
toGroupAndMember :: UTCTime -> Connection -> GroupInfoRow :. GroupMemberRow -> ((GroupInfo, GroupKeysRow), GroupMember)
toGroupAndMember currentTs c (groupInfoRow :. memberRow) =
let groupInfo = toGroupInfo currentTs cxt userContactId [] groupInfoRow
member = toGroupMember currentTs userContactId memberRow
+10 -8
View File
@@ -91,11 +91,12 @@ createOrUpdateContactRequest
pure $ RSAcceptedRequest cr (REContact ct)
Nothing ->
liftIO (getAcceptedBusinessChat xContactId) >>= \case
Just gInfo@GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do
Just (gInfo@GroupInfo {businessChat = Just BusinessChatInfo {customerId}}, keysData) -> do
clientMember <- getGroupMemberByMemberId db cxt user gInfo customerId
cr <- liftIO $ getContactRequestByXContactId xContactId
pure $ RSAcceptedRequest cr (REBusinessChat gInfo clientMember)
Just GroupInfo {businessChat = Nothing} -> throwError SEInvalidBusinessChatContactRequest
gks <- mkGroupKeys db cxt gInfo keysData
pure $ RSAcceptedRequest cr (REBusinessChat (GIK gInfo gks) clientMember)
Just (GroupInfo {businessChat = Nothing}, _) -> throwError SEInvalidBusinessChatContactRequest
-- 2) if no legacy accepted contact or business chat was found, next we try to find an existing request
Nothing ->
liftIO (getContactRequestByXContactId xContactId) >>= \case
@@ -131,7 +132,7 @@ createOrUpdateContactRequest
|]
(userId, xContactId)
mapM (addDirectChatTags db) ct_
getAcceptedBusinessChat :: XContactId -> IO (Maybe GroupInfo)
getAcceptedBusinessChat :: XContactId -> IO (Maybe (GroupInfo, GroupKeysRow))
getAcceptedBusinessChat xContactId = do
currentTs <- getCurrentTime
g_ <-
@@ -140,7 +141,7 @@ createOrUpdateContactRequest
db
(groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?")
(xContactId, userId, userContactId)
mapM (addGroupChatTags db) g_
forM g_ $ \(g, keysData) -> (,keysData) <$> addGroupChatTags db g
getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest)
getContactRequestByXContactId xContactId = do
currentTs <- getCurrentTime
@@ -214,7 +215,7 @@ createOrUpdateContactRequest
pure $ RSCurrentRequest Nothing ucr (Just $ REContact ct)
createBusinessChat = do
let groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs $ preferences' user
(gInfo@GroupInfo {groupId}, clientMember) <-
(gInfo@(GIK GroupInfo {groupId} _), clientMember) <-
createBusinessRequestGroup db cxt gVar user cReqChatVRange profile profileId ldn groupPreferences
liftIO $
DB.execute
@@ -302,11 +303,12 @@ createOrUpdateContactRequest
ct <- getContact db cxt user contactId
pure $ Just (REContact ct)
(Nothing, Just businessGroupId) -> do
gInfo <- getGroupInfo db cxt user businessGroupId
(gInfo, keysData) <- getGroupInfoRow db cxt user businessGroupId
case gInfo of
GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do
clientMember <- getGroupMemberByMemberId db cxt user gInfo customerId
pure $ Just (REBusinessChat gInfo clientMember)
gks <- mkGroupKeys db cxt gInfo keysData
pure $ Just (REBusinessChat (GIK gInfo gks) clientMember)
_ -> throwError SEInvalidBusinessChatContactRequest
(Nothing, Nothing) -> pure Nothing
_ -> throwError $ SEInvalidContactRequestEntity contactRequestId
+1 -1
View File
@@ -188,7 +188,7 @@ createConnReqConnection db userId acId preparedEntity_ cReq cReqHash sLnk xConta
connId <- insertedRowId db
case preparedEntity_ of
-- For relay groups, setPreparedGroupLinkInfo_ is called via updatePreparedRelayedGroup before the relay loop
Just (PCEGroup gInfo _) | not (useRelays' gInfo) ->
Just (PCEGroup (GIK gInfo _) _) | not (useRelays' gInfo) ->
setPreparedGroupLinkInfo_ db gInfo cReq cReqHash customUserProfileId Nothing currentTs
_ -> pure ()
pure
+49 -68
View File
@@ -40,6 +40,7 @@ module Simplex.Chat.Store.Groups
createGroupRejectedViaLink,
setGroupInvitationChatItemId,
getGroup,
getGroupKeys_,
getGroupInfoByUserContactLinkConnReq,
getGroupInfoViaUserTarget,
getGroupViaShortLinkToConnect,
@@ -144,7 +145,7 @@ module Simplex.Chat.Store.Groups
updatePreparedRelayedGroup,
updatePublicMemberCount,
setPublicMemberCount,
updateGroupMemberKeys,
setGroupRootKey,
updateRelayGroupKeys,
updateGroupMemberStatus,
updateGroupMemberStatusById,
@@ -376,26 +377,24 @@ setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CC
pure gLnk {connLinkContact = CCLink connFullLink (Just shortLink), shortLinkDataSet = True, shortLinkLargeDataSet = BoolDef True}
-- | creates completely new group with a single member - the current user
createNewGroup :: DB.Connection -> StoreCxt -> User -> GroupProfile -> Maybe Profile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo
createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays memberId groupKeys publicMemberCount_ = ExceptT $ do
createNewGroup :: DB.Connection -> StoreCxt -> User -> GroupProfile -> Maybe Profile -> MemberId -> GroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo
createNewGroup db cxt user@User {userId} groupProfile incognitoProfile memberId groupKeys publicMemberCount_ = ExceptT $ do
let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} = groupProfile
(groupType_, groupLink_, publicGroupId_) = case publicGroup of
Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId)
Nothing -> (Nothing, Nothing, Nothing)
fullGroupPreferences = mergeGroupPreferences groupPreferences
useRelays = isPublicGroup groupKeys
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
let (rootPrivKey_, rootPubKey_, memberPrivKey_) = case groupKeys of
Nothing -> (Nothing, Nothing, Nothing)
Just GroupKeys {publicGroupKeys, memberPrivKey} ->
let (rpk, rpub) = case publicGroupKeys of
Just PublicGroupKeys {groupRootKey} -> case groupRootKey of
GRKPrivate pk -> (Just pk, Nothing)
GRKPublic k -> (Nothing, Just k)
Nothing -> (Nothing, Nothing)
in (rpk, rpub, Just memberPrivKey)
let (rootPrivKey_, rootPubKey_) = case groupKeys of
GKPublicGroup {groupRootKey} -> case groupRootKey of
GRKPrivate pk -> (Just pk, Nothing)
GRKPublic k -> (Nothing, Just k)
_ -> (Nothing, Nothing)
memberPrivKey_ = Just $ memberPrivKey groupKeys
groupId <- liftIO $ do
DB.execute
db
@@ -423,7 +422,7 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays
:. (rootPrivKey_, rootPubKey_, memberPrivKey_, publicMemberCount_, rosterVersion0)
)
insertedRowId db
let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys
let memberPubKey = Just $ C.publicKey $ memberPrivKey groupKeys
membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole memberId GROwner) GCUserMember GSMemCreator IBUser customUserProfileId memberPubKey currentTs (vr cxt)
let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False}
pure
@@ -451,18 +450,17 @@ createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays
customData = Nothing,
membersRequireAttention = 0,
viaGroupLinkUri = Nothing,
groupKeys,
groupDomainVerified = Nothing
}
-- | creates a new group record for the group the current user was invited to, or returns an existing one
createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> C.KeyPairEd25519 -> ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> C.KeyPairEd25519 -> ExceptT StoreError IO (GroupInfoKeys, GroupMemberId)
createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ _ = throwError $ SEContactNotReady localDisplayName
createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, activeConn = Just Connection {peerChatVRange}} GroupInvitation {fromMember, fromMemberKey, invitedMember, connRequest, groupProfile, business} incognitoProfileId memberKeys = do
liftIO getInvitationGroupId_ >>= \case
Nothing -> createGroupInvitation_
Just gId -> do
gInfo@GroupInfo {membership, groupProfile = p'} <- getGroupInfo db cxt user gId
GIK gInfo@GroupInfo {membership, groupProfile = p'} gks <- getGroupInfoKeys db cxt user gId
hostId <- getHostMemberId_ db user gId
let GroupMember {groupMemberId, memberId, memberRole} = membership
MemberIdRole {memberId = invMemberId, memberRole = invMemberRole} = invitedMember
@@ -472,13 +470,13 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti
if p' == groupProfile
then pure gInfo
else updateGroupProfile db user gInfo groupProfile
pure (gInfo', hostId)
pure (GIK gInfo' gks, hostId)
where
getInvitationGroupId_ :: IO (Maybe Int64)
getInvitationGroupId_ =
maybeFirstRow fromOnly $
DB.query db "SELECT group_id FROM groups WHERE inv_queue_info = ? AND user_id = ? LIMIT 1" (connRequest, userId)
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfo, GroupMemberId)
createGroupInvitation_ :: ExceptT StoreError IO (GroupInfoKeys, GroupMemberId)
createGroupInvitation_ = do
let GroupProfile {displayName, fullName, shortDescr, description, image, groupPreferences, memberAdmission} = groupProfile
fullGroupPreferences = mergeGroupPreferences groupPreferences
@@ -530,9 +528,9 @@ createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, acti
customData = Nothing,
membersRequireAttention = 0,
viaGroupLinkUri = Nothing,
groupKeys = Just GroupKeys {publicGroupKeys = Nothing, memberPrivKey = snd memberKeys},
groupDomainVerified = Nothing
},
}
`GIK` GKGroup {memberPrivKey = snd memberKeys},
groupMemberId
)
@@ -946,10 +944,13 @@ setGroupInvitationChatItemId db User {userId} groupId chatItemId = do
-- TODO return the last connection that is ready, not any last connection
-- requires updating connection status
getGroup :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO Group
getGroup db cxt user groupId = do
gInfo <- getGroupInfo db cxt user groupId
getGroup db cxt user groupId = fst <$> getGroupKeys_ db cxt user groupId
getGroupKeys_ :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO (Group, GroupKeys)
getGroupKeys_ db cxt user groupId = do
GIK gInfo gks <- getGroupInfoKeys db cxt user groupId
members <- liftIO $ getGroupMembers db cxt user gInfo
pure $ Group gInfo members
pure (Group gInfo members, gks)
deleteGroupChatItems :: DB.Connection -> User -> GroupInfo -> IO ()
deleteGroupChatItems db User {userId} GroupInfo {groupId} =
@@ -1054,7 +1055,7 @@ getInProgressGroups db cxt user@User {userId} createdAtCutoff = do
getBaseGroupDetails :: DB.Connection -> StoreCxt -> User -> Maybe ContactId -> Maybe Text -> IO [GroupInfo]
getBaseGroupDetails db cxt User {userId, userContactId} _contactId_ search_ = do
currentTs <- getCurrentTime
map (toGroupInfo currentTs cxt userContactId [])
map (toGroupInfo_ currentTs cxt userContactId [])
<$> DB.query db (groupInfoQuery <> " " <> condition) (userId, userContactId, search, search, search, search)
where
condition =
@@ -1361,11 +1362,11 @@ getGroupInvitation :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT St
getGroupInvitation db cxt user groupId =
getConnRec_ user >>= \case
Just connRequest -> do
groupInfo@GroupInfo {membership} <- getGroupInfo db cxt user groupId
GIK groupInfo@GroupInfo {membership} groupKeys <- getGroupInfoKeys db cxt user groupId
when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined
hostId <- getHostMemberId_ db user groupId
fromMember <- getGroupMember db cxt user groupId hostId
pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo}
pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo, groupKeys}
_ -> throwError SEGroupInvitationNotFound
where
getConnRec_ :: User -> ExceptT StoreError IO (Maybe ConnReqInvitation)
@@ -1695,12 +1696,6 @@ 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
@@ -1914,13 +1909,13 @@ createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMemb
groupPreferences = Nothing,
memberAdmission = Nothing
}
(groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just relayStatus) Nothing Nothing currentTs
(_, memberPrivKey) <- atomically $ C.generateKeyPair (drg cxt)
(groupId, _groupLDN) <- createGroup_ db userId placeholderProfile Nothing Nothing True (Just relayStatus) Nothing (Just memberPrivKey) currentTs
-- Store relay request data for recovery
liftIO $ setRelayRequestData_ groupId currentTs
ownerMemberId <- insertOwner_ currentTs groupId
let relayMember = MemberIdRole relayMemberId GRRelay
-- TODO [member keys] should relays use member keys?
_membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember memberStatus IBUnknown Nothing Nothing currentTs (vr cxt)
_membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember memberStatus IBUnknown Nothing (Just $ C.publicKey memberPrivKey) currentTs (vr cxt)
ownerMember <- getGroupMember db cxt user groupId ownerMemberId
g <- getGroupInfo db cxt user groupId
pure (g, ownerMember)
@@ -2015,16 +2010,17 @@ isRelayGroupRejected db User {userId} groupLink =
(userId, groupLink, RSRejected)
)
getRelayServedGroups :: DB.Connection -> StoreCxt -> User -> IO [GroupInfo]
getRelayServedGroups :: DB.Connection -> StoreCxt -> User -> ExceptT StoreError IO [GroupInfoKeys]
getRelayServedGroups db cxt User {userId, userContactId} = do
currentTs <- getCurrentTime
map (toGroupInfo currentTs cxt userContactId [])
currentTs <- liftIO getCurrentTime
rows <- liftIO $ map (toGroupInfo currentTs cxt userContactId [])
<$> DB.query
db
( groupInfoQuery
<> " WHERE g.user_id = ? AND mu.contact_id = ? AND g.relay_own_status IN (?, ?, ?)"
)
(userId, userContactId, RSAccepted, RSAcknowledgedRoster, RSActive)
forM rows $ \(g, keysData) -> GIK g <$> mkGroupKeys db cxt g keysData
getRelayPublishableGroups :: DB.Connection -> User -> IO [(Int64, B64UrlByteString, Maybe PublicGroupAccess)]
getRelayPublishableGroups db User {userId, userContactId} =
@@ -2062,7 +2058,7 @@ getRelayInactiveGroups :: DB.Connection -> StoreCxt -> User -> NominalDiffTime -
getRelayInactiveGroups db cxt User {userId, userContactId} ttl = do
currentTs <- getCurrentTime
let cutoffTs = addUTCTime (- ttl) currentTs
map (toGroupInfo currentTs cxt userContactId [])
map (toGroupInfo_ currentTs cxt userContactId [])
<$> DB.query
db
( groupInfoQuery
@@ -2149,7 +2145,7 @@ createJoiningMemberConnection
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just uclId) Nothing 0 createdAt subMode PQSupportOff
setCommandConnId db user cmdId connId
createBusinessRequestGroup :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> VersionRangeChat -> Profile -> Int64 -> Text -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember)
createBusinessRequestGroup :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> VersionRangeChat -> Profile -> Int64 -> Text -> GroupPreferences -> ExceptT StoreError IO (GroupInfoKeys, GroupMember)
createBusinessRequestGroup
db
cxt
@@ -2164,7 +2160,7 @@ createBusinessRequestGroup
(groupId, membership@GroupMember {memberId = userMemberId}) <- insertGroup_ currentTs
(groupMemberId, memberId) <- insertClientMember_ currentTs groupId membership
liftIO $ DB.execute db "UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ?" (userMemberId, memberId, groupId)
groupInfo <- getGroupInfo db cxt user groupId
groupInfo <- getGroupInfoKeys db cxt user groupId
clientMember <- getGroupMemberById db cxt user groupMemberId
pure (groupInfo, clientMember)
where
@@ -2250,14 +2246,14 @@ createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentCon
-- which is used in single-connection flows.
updatePreparedRelayedGroup ::
DB.Connection -> StoreCxt -> User -> GroupInfo -> ConnReqContact -> ConnReqUriHash -> Maybe Profile ->
C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> Maybe Int64 ->
ExceptT StoreError IO GroupInfo
updatePreparedRelayedGroup db cxt user@User {userId} gInfo cReq cReqHash incognitoProfile rootPubKey memberPrivKey publicMemberCount_ = do
C.PublicKeyEd25519 -> Maybe Int64 ->
ExceptT StoreError IO GroupInfoKeys
updatePreparedRelayedGroup db cxt user@User {userId} gInfo cReq cReqHash incognitoProfile rootPubKey publicMemberCount_ = do
currentTs <- liftIO getCurrentTime
customUserProfileId <- liftIO $ mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile
liftIO $ setPreparedGroupLinkInfo_ db gInfo cReq cReqHash customUserProfileId publicMemberCount_ currentTs
liftIO $ updateGroupMemberKeys db (groupId' gInfo) rootPubKey memberPrivKey (groupMemberId' $ membership gInfo)
getGroupInfo db cxt user (groupId' gInfo)
liftIO $ setGroupRootKey db (groupId' gInfo) rootPubKey
getGroupInfoKeys db cxt user (groupId' gInfo)
updatePublicMemberCount :: DB.Connection -> StoreCxt -> User -> GroupInfo -> ExceptT StoreError IO GroupInfo
updatePublicMemberCount db cxt user GroupInfo {groupId} = do
@@ -2284,23 +2280,15 @@ setPublicMemberCount db cxt user GroupInfo {groupId} publicCount = do
liftIO $ DB.execute db "UPDATE groups SET public_member_count = ?, updated_at = ? WHERE group_id = ?" (publicCount, currentTs, groupId)
getGroupInfo db cxt user groupId
updateGroupMemberKeys :: DB.Connection -> GroupId -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> GroupMemberId -> IO ()
updateGroupMemberKeys db groupId rootPubKey memberPrivKey membershipGMId = do
setGroupRootKey :: DB.Connection -> GroupId -> C.PublicKeyEd25519 -> IO ()
setGroupRootKey db groupId rootPubKey = do
currentTs <- getCurrentTime
DB.execute
db
"UPDATE groups SET root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?"
(rootPubKey, memberPrivKey, currentTs, groupId)
DB.execute
db
"UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?"
(C.publicKey memberPrivKey, currentTs, membershipGMId)
DB.execute db "UPDATE groups SET root_pub_key = ?, updated_at = ? WHERE group_id = ?" (rootPubKey, currentTs, groupId)
updateRelayGroupKeys :: DB.Connection -> User -> GroupInfo -> PublicGroupProfile -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> [OwnerAuth] -> ExceptT StoreError IO ()
updateRelayGroupKeys db user@User {userId} gInfo PublicGroupProfile {groupType, groupLink, publicGroupId} rootPubKey memberPrivKey owners = do
updateRelayGroupKeys :: DB.Connection -> User -> GroupInfo -> PublicGroupProfile -> C.PublicKeyEd25519 -> [OwnerAuth] -> ExceptT StoreError IO ()
updateRelayGroupKeys db user@User {userId} gInfo PublicGroupProfile {groupType, groupLink, publicGroupId} rootPubKey owners = do
currentTs <- liftIO getCurrentTime
let membershipGMId = groupMemberId' $ membership gInfo
groupId = groupId' gInfo
let groupId = groupId' gInfo
liftIO $ do
DB.execute
db
@@ -2309,14 +2297,7 @@ updateRelayGroupKeys db user@User {userId} gInfo PublicGroupProfile {groupType,
WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?)
|]
(groupType, groupLink, publicGroupId, currentTs, userId, groupId)
DB.execute
db
"UPDATE groups SET root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?"
(rootPubKey, memberPrivKey, currentTs, groupId)
DB.execute
db
"UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?"
(C.publicKey memberPrivKey, currentTs, membershipGMId)
setGroupRootKey db groupId rootPubKey
-- TODO [relays] relay: if not found, create owner record (multi-owner)
forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> do
ownerGMId <- getGroupMemberIdViaMemberId db user gInfo (MemberId ownerId)
@@ -1870,6 +1870,15 @@ Query:
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE groups
SET member_priv_key = COALESCE(member_priv_key, ?), updated_at = ?
WHERE group_id = ?
RETURNING member_priv_key
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE groups
SET relay_request_inv_id = ?,
@@ -7587,10 +7596,22 @@ Query: SELECT member_id FROM group_members WHERE member_role = ? LIMIT 1
Plan:
SCAN group_members
Query: SELECT member_priv_key FROM groups
Plan:
SCAN groups
Query: SELECT member_pub_key FROM group_members WHERE local_display_name = ?
Plan:
SCAN group_members
Query: SELECT member_pub_key FROM group_members WHERE member_category = 'host'
Plan:
SCAN group_members
Query: SELECT member_pub_key FROM group_members WHERE member_category = 'user'
Plan:
SCAN group_members
Query: SELECT member_pub_key FROM group_members WHERE member_role = 'moderator'
Plan:
SCAN group_members
@@ -7967,6 +7988,14 @@ Query: UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_m
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE group_members SET member_pub_key = NULL WHERE member_category = 'host'
Plan:
SCAN group_members
Query: UPDATE group_members SET member_pub_key = NULL WHERE member_category = 'user'
Plan:
SCAN group_members
Query: UPDATE group_members SET member_relations_vector = set_member_vector_new_relation(member_relations_vector, ?, ?, ?), updated_at = ? WHERE group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
@@ -8059,6 +8088,10 @@ Query: UPDATE groups SET local_display_name = ?, updated_at = ? WHERE user_id =
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET member_priv_key = NULL
Plan:
SCAN groups
Query: UPDATE groups SET members_require_attention=1 WHERE group_id=?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
@@ -8079,7 +8112,7 @@ Query: UPDATE groups SET request_shared_msg_id = ? WHERE group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET root_pub_key = ?, member_priv_key = ?, updated_at = ? WHERE group_id = ?
Query: UPDATE groups SET root_pub_key = ?, updated_at = ? WHERE group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
+46 -11
View File
@@ -694,18 +694,21 @@ type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, Ver
type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences) :. BadgeRow :. ContactDomainRow
toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> (GroupInfo, GroupKeysRow)
toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, rosterVersion, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri, groupDomainVerified) :. groupKeysRow :. userMemberRow) =
let membership = (toGroupMember now userContactId userMemberRow) {memberChatVRange = vr cxt}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences
publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ (toPublicGroupAccess accessRow)
groupKeys = toGroupKeys publicGroupId_ groupKeysRow
groupProfile = GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission}
businessChat = toBusinessChatInfo (toPublicGroupAccess accessRow >>= groupDomainClaim) businessRow
preparedGroup = toPreparedGroup preparedGroupRow
groupSummary = GroupSummary {currentMembers, publicMemberCount}
in GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupKeys, groupDomainVerified = unBI <$> groupDomainVerified}
gInfo = GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupDomainVerified = unBI <$> groupDomainVerified}
in (gInfo, groupKeysRow)
toGroupInfo_ :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo_ now cxt userContactId chatTags row = fst $ toGroupInfo now cxt userContactId chatTags row
toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup
toPreparedGroup = \case
@@ -733,13 +736,35 @@ toPublicGroupAccess (groupWebPage, groupDomain_, domainWebPage_, allowEmbedding_
domainWebPage = maybe False unBI domainWebPage_
allowEmbedding = maybe False unBI allowEmbedding_
toGroupKeys :: Maybe B64UrlByteString -> GroupKeysRow -> Maybe GroupKeys
toGroupKeys publicGroupId_ (rootPrivKey, rootPubKey, memberPrivKey) =
let publicGroupKeys = case (publicGroupId_, GRKPrivate <$> rootPrivKey <|> GRKPublic <$> rootPubKey) of
(Just publicGroupId, Just groupRootKey) -> Just $ Just PublicGroupKeys {publicGroupId, groupRootKey}
(Nothing, Nothing) -> Just Nothing
_ -> Nothing -- invalid state, in which case messages won't be signed even if memberPrivKey is present
in GroupKeys <$> publicGroupKeys <*> memberPrivKey
mkGroupKeys :: DB.Connection -> StoreCxt -> GroupInfo -> GroupKeysRow -> ExceptT StoreError IO GroupKeys
mkGroupKeys db cxt g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup}, membership} (rootPrivKey, rootPubKey, memberPrivKey_) = do
memberPrivKey <- case memberPrivKey_ of
Just k -> pure k
Nothing -> do
(_, k) <- atomically $ C.generateKeyPair (drg cxt)
setUserMemberKey db groupId (groupMemberId' membership) k
pure $ case (useRelays' g, isJust publicGroup, GRKPrivate <$> rootPrivKey <|> GRKPublic <$> rootPubKey) of
(False, _, _) -> GKGroup {memberPrivKey}
(True, True, Just groupRootKey) -> GKPublicGroup {groupRootKey, memberPrivKey}
(True, True, Nothing) -> GKPreparedPublicGroup {memberPrivKey}
(True, False, _) -> GKRelayRequest {memberPrivKey}
setUserMemberKey :: DB.Connection -> GroupId -> GroupMemberId -> C.PrivateKeyEd25519 -> ExceptT StoreError IO C.PrivateKeyEd25519
setUserMemberKey db groupId membershipId newKey = do
currentTs <- liftIO getCurrentTime
memberPrivKey <-
ExceptT . firstRow fromOnly (SEGroupNotFound groupId) $
DB.query
db
[sql|
UPDATE groups
SET member_priv_key = COALESCE(member_priv_key, ?), updated_at = ?
WHERE group_id = ?
RETURNING member_priv_key
|]
(newKey, currentTs, groupId)
liftIO $ DB.execute db "UPDATE group_members SET member_pub_key = ?, updated_at = ? WHERE group_member_id = ?" (C.publicKey memberPrivKey, currentTs, membershipId)
pure 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_)) =
@@ -901,8 +926,18 @@ addGroupChatTags db g@GroupInfo {groupId} = do
chatTags <- getGroupChatTags db groupId
pure (g :: GroupInfo) {chatTags}
getGroupInfoKeys :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO GroupInfoKeys
getGroupInfoKeys db cxt user groupId = do
(g@GroupInfo {membership}, keysData) <- getGroupInfoRow db cxt user groupId
gks <- mkGroupKeys db cxt g keysData
let membership' = membership {memberPubKey = Just $ C.publicKey $ memberPrivKey gks} :: GroupMember
pure $ GIK (g :: GroupInfo) {membership = membership'} gks
getGroupInfo :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO GroupInfo
getGroupInfo db cxt User {userId, userContactId} groupId = ExceptT $ do
getGroupInfo db cxt user groupId = fst <$> getGroupInfoRow db cxt user groupId
getGroupInfoRow :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO (GroupInfo, GroupKeysRow)
getGroupInfoRow db cxt User {userId, userContactId} groupId = ExceptT $ do
currentTs <- getCurrentTime
chatTags <- getGroupChatTags db groupId
firstRow (toGroupInfo currentTs cxt userContactId chatTags) (SEGroupNotFound groupId) $
+33 -21
View File
@@ -30,7 +30,9 @@
module Simplex.Chat.Types where
import Control.Applicative ((<|>))
import Control.Concurrent.STM (TVar)
import Crypto.Number.Serialize (os2ip)
import Crypto.Random (ChaChaDRG)
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.Encoding as JE
@@ -437,7 +439,7 @@ instance ToJSON ConnReqUriHash where
data RequestEntity
= REContact Contact
| REBusinessChat GroupInfo GroupMember
| REBusinessChat GroupInfoKeys GroupMember
type RepeatRequest = Bool
@@ -479,17 +481,30 @@ groupRootPubKey :: GroupRootKey -> C.PublicKeyEd25519
groupRootPubKey (GRKPrivate pk) = C.publicKey pk
groupRootPubKey (GRKPublic pk) = pk
data GroupKeys = GroupKeys
{ publicGroupKeys :: Maybe PublicGroupKeys,
memberPrivKey :: C.PrivateKeyEd25519
}
data GroupKeys
= GKGroup
{ memberPrivKey :: C.PrivateKeyEd25519
}
| GKPublicGroup
{ groupRootKey :: GroupRootKey,
memberPrivKey :: C.PrivateKeyEd25519
}
| GKRelayRequest
{ memberPrivKey :: C.PrivateKeyEd25519
}
| GKPreparedPublicGroup
{ memberPrivKey :: C.PrivateKeyEd25519
}
deriving (Eq, Show)
data PublicGroupKeys = PublicGroupKeys
{ publicGroupId :: B64UrlByteString,
groupRootKey :: GroupRootKey
}
deriving (Eq, Show)
isPublicGroup :: GroupKeys -> Bool
isPublicGroup = \case
GKGroup {} -> False
GKPublicGroup {} -> True
GKRelayRequest {} -> True
GKPreparedPublicGroup {} -> True
data GroupInfoKeys = GIK GroupInfo GroupKeys
data GroupInfo = GroupInfo
{ groupId :: GroupId,
@@ -515,7 +530,6 @@ data GroupInfo = GroupInfo
rosterVersion :: Maybe VersionRoster,
membersRequireAttention :: Int,
viaGroupLinkUri :: Maybe ConnReqContact,
groupKeys :: Maybe GroupKeys,
groupDomainVerified :: Maybe Bool
}
deriving (Eq, Show)
@@ -523,6 +537,9 @@ data GroupInfo = GroupInfo
useRelays' :: GroupInfo -> Bool
useRelays' GroupInfo {useRelays} = isTrue useRelays
publicGroup' :: GroupInfo -> Maybe PublicGroupProfile
publicGroup' g@GroupInfo {groupProfile = GroupProfile {publicGroup}} = if useRelays' g then publicGroup else Nothing
relayServesGroup :: GroupInfo -> Bool
relayServesGroup GroupInfo {relayOwnStatus} = case relayOwnStatus of
Just RSInactive -> False
@@ -595,7 +612,7 @@ data GroupLink = GroupLink
data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember]
data PreparedChatEntity = PCEContact Contact | PCEGroup {groupInfo :: GroupInfo, hostMember :: GroupMember}
data PreparedChatEntity = PCEContact Contact | PCEGroup {groupInfo :: GroupInfoKeys, hostMember :: GroupMember}
contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId)
contactAndGroupIds = \case
@@ -1145,7 +1162,8 @@ memberRestrictions m
data ReceivedGroupInvitation = ReceivedGroupInvitation
{ fromMember :: GroupMember,
connRequest :: ConnReqInvitation,
groupInfo :: GroupInfo
groupInfo :: GroupInfo,
groupKeys :: GroupKeys
}
deriving (Eq, Show)
@@ -2240,8 +2258,8 @@ type VersionChat = Version ChatVersion
type VersionRangeChat = VersionRange ChatVersion
-- | Store-wide context passed to store functions in place of the bare `vr`
-- parameter. Built from config by mkStoreCxt; more fields are added here over time.
data StoreCxt = StoreCxt {vr :: VersionRangeChat, badgeKeys :: Map Int BBSPublicKey}
-- parameter. Built from config by storeCxt; more fields are added here over time.
data StoreCxt = StoreCxt {vr :: VersionRangeChat, badgeKeys :: Map Int BBSPublicKey, drg :: TVar ChaChaDRG}
pattern VersionChat :: Word16 -> VersionChat
pattern VersionChat v = Version v
@@ -2347,12 +2365,6 @@ instance FromJSON GroupSummary where
parseJSON = $(JQ.mkParseJSON defaultJSON ''GroupSummary)
omittedField = Just GroupSummary {currentMembers = 0, publicMemberCount = Nothing}
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GRK") ''GroupRootKey)
$(JQ.deriveJSON defaultJSON ''PublicGroupKeys)
$(JQ.deriveJSON defaultJSON ''GroupKeys)
$(JQ.deriveJSON defaultJSON ''GroupInfo)
$(JQ.deriveJSON defaultJSON ''Group)
+3 -3
View File
@@ -42,7 +42,7 @@ import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Data.Time.Clock (UTCTime, getCurrentTime)
import Simplex.Chat.Controller (ChatController (..), CorsOrigin (..), PublishableGroup (..), WebPreviewConfig (..), WebPreviewState (..), mkStoreCxt)
import Simplex.Chat.Controller (ChatController (..), CorsOrigin (..), PublishableGroup (..), WebPreviewConfig (..), WebPreviewState (..), storeCxt)
import Simplex.Chat.Markdown (FormattedText (..), MarkdownList, parseMaybeMarkdownList)
import Simplex.Chat.Messages
( CChatItem (..),
@@ -137,7 +137,7 @@ webPreviewWorker cfg@WebPreviewConfig {webJsonDir, webCorsFile, webUpdateInterva
seedRoutinePending wps
forever $ workerLoop wps `catchOwn` \e -> logError ("web preview worker error: " <> tshow e)
where
cxt = mkStoreCxt (config cc)
cxt = storeCxt cc
workerLoop wps@WebPreviewState {priorityRender, filesToRemove, corsNeeded, routinePending, wakeSignal} = do
drainRemovals
@@ -262,7 +262,7 @@ renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gI
pure $ corsEntry publicGroupId <$> publicGroupAccess
Nothing -> pure Nothing
where
cxt = mkStoreCxt (config cc)
cxt = storeCxt cc
channelContentChanged :: ChatController -> Int64 -> STM ()
channelContentChanged cc gId =